Compare commits

...

372 Commits

Author SHA1 Message Date
oscarzhou
2fb745c3f8 fix(swagger/doc): align fields in endpoint update payload and response 2023-05-01 17:28:48 +12:00
Prabhat Khera
731f3959c7 fix(UI): update application deploy/update messages EE-4005 (#8819) 2023-05-01 09:14:30 +12:00
cmeng
0f9a0e25f2 fix(login) hide password in console EE-5279 (#8774) 2023-04-29 07:24:33 +12:00
cmeng
ae339a0047 fix(stack) add skip TLS toggle for edit stack EE-5391 (#8851) 2023-04-28 13:35:33 +12:00
Chaim Lev-Ari
77f8b9333a refactor(stacks): break swagger docs by type [EE-5381] (#8820) 2023-04-27 11:03:55 +07:00
Chaim Lev-Ari
bbea0bc8a5 feat(edge): hide envs from waiting room [EE-5185] (#8688) 2023-04-27 09:23:10 +07:00
Chaim Lev-Ari
4b9c857d85 feat(waiting-room): show and filter by check in [EE-5186] (#8701) 2023-04-27 09:22:05 +07:00
Dakota Walsh
b5771df6a8 fix(image): allow dot in image names [EE-4595] (#8619) 2023-04-27 09:44:08 +12:00
matias-portainer
7ed8e9e167 fix(images): avoid returning null on registryId default value EE-5394 (#8841) 2023-04-26 10:24:45 -03:00
Prabhat Khera
80a3a5f16e feat(kubernetes): fix annotation validation EE-5021 (#8818) 2023-04-26 16:48:55 +12:00
Ali
3e654ff9b2 fix(deploy): return to referring view [EE-5345] (#8763)
* fix(deploy): return to referring view [EE-5345]

* no-underline -> no-decoration

---------

Co-authored-by: testa113 <testa113>
2023-04-26 11:23:15 +12:00
LP B
9b287f3020 fix(api/registry): encode X-Registry-Auth header using base64url instead of base64 [EE-4726] (#8492) 2023-04-24 13:57:39 +02:00
Oscar Zhou
a7404e00d1 fix(ci/security): intepret matrix summary as string not shell command (#8836) 2023-04-24 13:21:35 +12:00
Dakota Walsh
3654109332 fix(slider): update rc-slider [EE-5011] (#8611)
* fix(slider): update rc-slider [EE-5011]

* fix PasswordLengthSlider tooltip

* fix unnecessarily bulky className for SliderTooltip

* remove SliderTooltip inner div

* center slider handle value

* relative tooltip

* update z index

---------

Co-authored-by: testa113 <testa113>
2023-04-21 16:52:05 +12:00
Oscar Zhou
bf9dc8c2d0 feat(ci/security): remove deprecated github action command alert [EE-3059] (#8795) 2023-04-21 10:57:38 +12:00
cmeng
67f8e8f3c2 fix(webhook) remove NaN fom webhook url EE-5373 (#8816) 2023-04-21 10:56:53 +12:00
andres-portainer
56d6dfe02e feat(transactions): add transaction support for Registries EE-5382 (#8825) 2023-04-20 18:42:52 -03:00
Ali
861a9a5bbb fix(templates): update name validation [EE-5344] (#8760)
Co-authored-by: testa113 <testa113>
2023-04-21 09:39:55 +12:00
Matt Hook
1b470845b8 better logging during critical migration error (#8576) 2023-04-21 09:30:12 +12:00
Matt Hook
3c26aa8f34 feat(packages): upgrade packages [EE-5147] (#8658)
* upgrade packages

* update eksctl to match ee

* update helm to latest
2023-04-20 13:31:29 +12:00
Ali
de953da5a4 fix(editor): fix styles [EE-5369] (#8809)
* fix(editor): fix styles [EE-5369]

* rm hash

---------

Co-authored-by: testa113 <testa113>
2023-04-20 08:27:25 +12:00
Chaim Lev-Ari
5356d1feeb fix(edge/updates): add padding for edge groups [EE-5349] (#8772) 2023-04-18 13:40:12 +12:00
Matt Hook
7a8a20e0cc feat(libhelm): allow passing optional env and http client [EE-5252] (#8758) 2023-04-14 14:50:37 +12:00
andres-portainer
a7474188b9 chore(deps): unify dependencies EE-5360 (#8778) 2023-04-13 18:07:32 -03:00
cmeng
6fe56f89c6 fix(backup) add description text to backup EE-5283 (#8775) 2023-04-13 16:05:12 +12:00
Oscar Zhou
a98f480974 fix(swagger): correct endpoint api annotations [EE-5333] (#8761) 2023-04-13 15:31:27 +12:00
cmeng
8ccac7c98f fix(stack): upgrade docker-compose EE-5334 (#8757) 2023-04-11 17:56:00 +12:00
andres-portainer
e0ce3671e8 fix(tags): migrate to transactional code EE-5330 (#8755) 2023-04-10 19:03:51 -03:00
andres-portainer
62128d1069 fix(edgejobs): migrate to transactional code EE-5324 (#8747) 2023-04-10 15:59:34 -03:00
Oscar Zhou
a65ffe519a fix(k8s/gitops): missing git auth toggle in k8s app edit page [EE-5320] (#8741) 2023-04-10 20:14:13 +12:00
Ali
5ac1ea3df8 fix(ns): add selection caching back [EE-5273] (#8738)
Co-authored-by: testa113 <testa113>
2023-04-06 14:28:01 +12:00
Matt Hook
bf56bdb8f6 search for correct source directory when doing a restore (#8676) 2023-04-06 10:39:10 +12:00
cmeng
b00aa68c2b fix(homepage) move heartbeat logic to backend EE-5317 (#8737) 2023-04-06 09:09:22 +12:00
Matt Hook
8c5edd2c97 fix(docs): add missing swagger docs for upload file [EE-4886] (#8708)
* add docs for uploading files via host management features

* fix other doc issues
2023-04-04 16:59:34 +12:00
Oscar Zhou
c650868fe9 feat(templates): allow managing git based templates [EE-2600] (#7855)
Co-authored-by: itsconquest <william.conquest@portainer.io>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
2023-04-04 12:44:42 +12:00
cmeng
30a2bb0495 fix(security): potential vulnerability of path traversal attacks EE-5303 (#8728) 2023-04-04 09:00:17 +12:00
andres-portainer
1a451823d9 fix(edgestacks): fix a deadlock in UpdateEdgeStackFunc() (#8735) 2023-04-03 14:24:27 -03:00
Chaim Lev-Ari
feab2a757e feat(gitops): allow to skip tls verification [EE-5023] (#8668) 2023-04-03 09:19:17 +03:00
andres-portainer
17839aa473 fix(endpointrelation): change a callback so it is transactional EE-5312 (#8729) 2023-03-30 23:16:56 -03:00
Prabhat Khera
fc1aec3bb8 fix(ui): namespace caching issue EE-5273 (#8709)
* fix namespace caching issue

* fix(apps): add loading state [EE-5273]

* rm endpoint provider

* fix(namespace): remove caching [EE-5273]

* variable typo

---------

Co-authored-by: testa113 <testa113>
2023-03-31 13:24:57 +13:00
Chaim Lev-Ari
d64e7eacfc fix(ui/code-editor): stretch code editor content full height [EE-5202] (#8673) 2023-03-30 12:26:32 +03:00
Ali
7f805ac5be fix(ns): save filter to local storage [EE-5287] (#8723)
* fix(ns): save filter to local storage [EE-5287]

* allow system ns and save per user

---------

Co-authored-by: testa113 <testa113>
2023-03-30 11:21:05 +13:00
Chaim Lev-Ari
308a78db21 refactor(edge): deprecate IsEdgeDevice [EE-5046] (#8534) 2023-03-28 09:19:22 +03:00
andres-portainer
814fc9dfc0 fix(http): drain and close HTTP response bodies EE-5280 (#8716) 2023-03-27 15:14:16 -03:00
andres-portainer
3635df89dc fix(snapshots): change the snapshot object to maintain backwards compatibility EE-5240 (#8705) 2023-03-23 13:30:32 -03:00
Ali
30248eabb4 fix(apps) UI release fixes [EE-5197] (#8702)
* fix(apps) searchbar flex resizing and insights

* UI fixes

* update stacks datatable

---------

Co-authored-by: testa113 <testa113>
2023-03-23 08:20:30 +13:00
Ali
3636ac5c26 fix(dashboard): use faster proxy request [EE-5160] (#8693)
Co-authored-by: testa113 <testa113>
2023-03-22 15:34:44 +13:00
Prabhat Khera
f6e8b25cf3 fix Gpus null issue (#8692) 2023-03-21 16:06:01 +13:00
Oscar Zhou
124e0bf9b9 fix(stack/git): unexpected cursor movement in git text fields [EE-5143] (#8655) 2023-03-20 10:00:49 +13:00
Chaim Lev-Ari
45def82156 fix(ui/box-selector): BE link and use icons standard size [EE-5133] (#8607) 2023-03-19 13:37:35 +01:00
andres-portainer
76bdf6f220 fix(websocket): use the read part of the buffer instead of everything EE-5235 (#8685) 2023-03-17 17:23:24 -03:00
Ali
e142be399d fix(kubeconfig): fix download checkbox [EE-5199] (#8674)
Co-authored-by: testa113 <testa113>
2023-03-17 10:33:56 +13:00
Ali
13ba72ee07 fix(wizard): Capitalise Kubernetes [EE-5178] (#8662)
Co-authored-by: testa113 <testa113>
2023-03-16 18:50:54 +13:00
Dakota Walsh
f17a608dc7 fix(kubernetes): Prevent rerunning initial cluster detection [EE-5170] (#8666) 2023-03-16 15:39:26 +13:00
Prabhat Khera
6ee5cc6a56 fix(ui): namespace cache refresh on reload EE-5155 (#8644) 2023-03-16 10:10:37 +13:00
andres-portainer
44582732bb fix(home): exclude snapshots from the home page to improve the loading times EE-5154 (#8626) 2023-03-15 15:16:41 -03:00
andres-portainer
ea03024fbc fix(edgegroup): fix data race in edge group update EE-4441 (#8523) 2023-03-15 14:53:38 -03:00
Oscar Zhou
795e6a5b3c fix(stack/git): unable to move git repository error [EE-5144] (#8618) 2023-03-15 12:54:09 +13:00
andres-portainer
2b17cb9104 fix(kubernetes): fix data-race in GetKubeClient() EE-4436 (#8498) 2023-03-14 20:11:28 -03:00
andres-portainer
347f66b1f1 fix(edge): fix status inspect error message EE-5190 (#8661) 2023-03-14 13:28:20 -03:00
Ali
40c387f4f4 fix(annotations) ingress tip to match ee [EE-5158] (#8653)
Co-authored-by: testa113 <testa113>
2023-03-14 10:41:35 +13:00
andres-portainer
15cbdb8af9 chore(portainer): clean up the code EE-5188 (#8660) 2023-03-13 13:18:28 -03:00
matias-portainer
621a01ba3b fix(upgrade): remove yellow upgrade banner EE-5141 (#8640) 2023-03-13 09:01:27 -03:00
Ali
37f382d286 fix(kube): check for ns on enter [EE-5160] (#8647)
Co-authored-by: testa113 <testa113>
2023-03-13 13:57:11 +13:00
Prabhat Khera
77b49ae9c5 fix typo in delete image modal dialog (#8621) 2023-03-13 11:05:51 +13:00
Matt Hook
29648f517b reduce throttling in the kube client (#8630) 2023-03-13 09:44:27 +13:00
Ali
8f42af49e8 fix(annotation): update wording/styling [EE-5158] (#8642)
Co-authored-by: testa113 <testa113>
2023-03-10 16:52:09 +13:00
cmeng
0ab7987684 fix(edge-stack) always show edge group selector [EE-5157] (#8639) 2023-03-10 10:48:44 +13:00
Ali
31d956dbcb fix(app): restrict ns fix create app [EE-5123] (#8597)
* fix(app): restrict ns fix create app [EE-5123]

* fix node limits race condition

---------

Co-authored-by: testa113 <testa113>
2023-03-10 10:24:23 +13:00
Ali
2cc80e5e5d refactor(GPU): refactor to colocate and simplify UI work [EE-5127] (#8593)
* refactor to colocate and simplify

* fix(insights): text size to match portainer views

---------

Co-authored-by: testa113 <testa113>
2023-03-09 22:06:57 +13:00
matias-portainer
fb6e26a302 fix(stacks): pass WorkingDir to deployer command EE-5142 (#8615) 2023-03-08 19:34:57 -03:00
Matt Hook
9cca299833 fix(gotest): fix go tests as part of version bump to 2.19.0 (#8623)
* bump version to 2.19

* fix broken go tests
2023-03-08 17:23:34 +13:00
Chaim Lev-Ari
4c86be725d feat(system): upgrade portainer on kubernetes [EE-4625] (#8448) 2023-03-07 23:34:55 -03:00
Chaim Lev-Ari
0669ad77d3 fix(home): disable live connect for async [EE-5000] (#8462) 2023-03-07 21:27:34 -03:00
Matt Hook
2bfc956f58 bump version to 2.19 (#8617) 2023-03-08 13:24:59 +13:00
Ali
89194405ee fix(kube): improve dashboard load speed [EE-4941] (#8572)
* apply changes from EE

* clear query cache when logging out

* Text transitions in smoother
2023-03-08 11:22:08 +13:00
Chaim Lev-Ari
5f0af62521 fix(git): fix more files form error [EE-5125] (#8601) 2023-03-07 18:44:26 -03:00
Dakota Walsh
e3299eddd5 fix(docker): container health table [EE-5008] (#8591) 2023-03-08 10:33:15 +13:00
Prabhat Khera
bdde278139 feat(applications): application page performance improvements EE-4956 (#8569) 2023-03-08 10:27:42 +13:00
Chaim Lev-Ari
01ea9afe33 feat(edge): hide FDO features behind a flag [EE-5128] (#8600) 2023-03-07 13:45:39 -03:00
matias-portainer
8345d1471e fix(stacks): remove old dependency from stacks redeploy controller EE-5138 (#8609) 2023-03-07 13:35:43 -03:00
Prabhat Khera
2a55d20eff fix(annotations): minor issues in Annotations EE-4089 (#8612) 2023-03-07 21:09:41 +13:00
Ali
7dca784ec6 fix(env): get edited environment info [EE-5129] (#8603)
Co-authored-by: testa113 <testa113>
2023-03-07 14:44:19 +13:00
Oscar Zhou
37484566eb fix(ui/wizard): edge agent ui issue [EE-5126] (#8598) 2023-03-07 10:27:40 +13:00
Chaim Lev-Ari
70710cfeb7 feat(edge): associate edge env to meta fields [EE-3209] (#8551)
* refactor(edge/groups): load edge groups in selector

fix(edge/stacks): remove double groups title

* feat(edge): supply meta fields to edge script [EE-5043]

* feat(edge): auto assign aeec envs to groups and tags [EE-5043]

fix [EE-5043]

fix(envs): fix global key test

* fix(edge/groups): save group type

* refactor(edge/devices): move loading of devices to table

* refactor(tags): select paramter for query

* feat(edge/devices): show meta fields

* refactor(home): simplify filter

* feat(edge/devices): filter by meta fields

* refactor(edge/devices): break filter and loading hook
2023-03-07 09:25:04 +13:00
Chaim Lev-Ari
03712966e4 fix(ui/code-editor): apply theme colors [EE-5104] (#8558)
* fix(ui/code-editor): apply theme colors [EE-5104]

fix [EE-5104]

* fix(kube/yaml): expand yaml inspector

* fix(ui/code-editor): have default height

* fix(ui/code-editor): boolean color

* fix(ui/code-editor): style search bar
2023-03-06 09:13:42 +02:00
Chaim Lev-Ari
07100258cd fix(git): file path background [EE-5114] (#8573)
* fix(git): file path background [EE-5114]

also disabled url check on CE because
the http handler wasn't available and raised errors

* fix(git): highlight hovered path option

* feat(git): hide path options when choosing
2023-03-06 09:13:22 +02:00
cmeng
4c6f5f961e fix(wizard): fix tunnel add validation [EE-5124] (#8595) 2023-03-06 10:43:08 +13:00
Chaim Lev-Ari
77e1f5aa34 fix(oauth): danger confirm for hiding prompt [EE-4576] (#8574) 2023-03-05 15:22:49 +02:00
Chaim Lev-Ari
3baab6d695 fix(ui): import components for envs and registries [EE-5120] (#8583) 2023-03-05 15:22:00 +02:00
Chaim Lev-Ari
d546ff269b fix(ui/input-group): remove z-index [EE-5121] (#8582) 2023-03-05 12:14:08 +02:00
cmeng
60275dd31c feat(edge): EE-4621 support high latency for tunnel (#8302) 2023-03-04 09:13:37 +13:00
Ali
07df4b1591 fix(notifications): limit only in header [EE-4815] (#8579)
Co-authored-by: testa113 <testa113>
2023-03-03 15:08:15 +13:00
Ali
fd916bc8a2 feat(gpu): rework docker GPU for UI performance [EE-4918] (#8518) 2023-03-03 14:47:10 +13:00
Ali
769c8372fb feat(microk8s): BE teasers in wizard [EE-4772] (#8449)
* feat(microk8s): separate existing/new envs EE-4772

update icons

fix be teaser box selector style

* match environment wizard box selectors

* revert title back

* updated kaas description in wizard

---------

Co-authored-by: testa113 <testa113>
Co-authored-by: Prabhat Khera <prabhat.khera@portainer.io>
2023-03-03 12:27:24 +13:00
James Carppe
d032119ebc Update docs URL and add --log-level docs (#8511) 2023-03-03 11:38:19 +13:00
Matt Hook
ac47649631 feat(kubernetes): list all kube services screen [EE-1571] (#8524)
* port services from ee

* fix external link

* post review improvements

* remove applications-ports-datatable

* minor post review updates

* add services help url

* post review update

* more post review updates

* post review updates

* rename index to component

* fix external ip display and sorting

* fix external apps tag

* fix ingress screen time format

* use uid for row id. Prevent blank link

* fix some missing bits ported from EE

* match ee

* fix display of show system resources

* remove icon next to service type
2023-03-03 08:45:19 +13:00
Chaim Lev-Ari
8d6797dc9f fix(edge): show edge settings [EE-4959] (#8581)
* fix(edge): show edge settings [EE-4959]

fix [EE-4959]

* fix(edge/settings): validate ce
2023-03-02 19:32:43 +02:00
andres-portainer
197b0bcbde fix(code): add missing returns after sending HTTP errors EE-4442 (#7868) 2023-03-02 12:52:10 -03:00
Chaim Lev-Ari
6918da2414 refactor(stacks): extract auto update logic [EE-4945] (#8545) 2023-03-02 17:07:50 +02:00
cmeng
085381e6fc fix(logs-viewer): fail to search json logs [EE-4857] (#8482) 2023-03-02 23:55:13 +13:00
Chaim Lev-Ari
6074d1fcb5 fix(docker): clone config [EE-5115] (#8575) 2023-03-02 11:38:21 +02:00
Ali
96e5d44cc2 fix(teaser): add up-to-date teaser [EE-5014] (#8548)
Co-authored-by: testa113 <testa113>
2023-03-02 10:14:50 +13:00
matias-portainer
a45ef3d72e fix(azure): ensure azure client HTTPS verification EE-4444 (#8137) 2023-03-01 17:52:29 -03:00
Chaim Lev-Ari
c819d4e7f7 feat(environments): create async edge [EE-4480] (#8527) 2023-03-01 20:33:05 +02:00
LP B
bc6a667a6b feat(api/snapshot): extend docker container snapshot type (#8537) 2023-03-01 17:33:40 +01:00
matias-portainer
7dcd6f9b9e fix(ui): fix search in associated endpoints selector EE-4532 (#8454) 2023-03-01 10:34:07 -03:00
Chaim Lev-Ari
c8d334e603 fix(server): skip file deletion errors [EE-4916] (#8443) 2023-03-01 13:44:34 +02:00
Chaim Lev-Ari
ab9b0c2147 feat(docker/containers): show name in log view [EE-5108] (#8557) 2023-03-01 09:37:23 +02:00
cmeng
6d659b4a2c fix(edge-stack): add validation for edge stack name [EE-4283] (#8504) 2023-03-01 19:51:08 +13:00
Prabhat Khera
defce0cf6d feat(kuberenetes): add annotations to kube objects EE-4089 (#8499)
* add annotations BE teaser
* fix settings icon click on home screen for kube env
* add debouce to namespace validation
* ingress button tooltip fixed
* fix tooltip text
2023-03-01 13:11:12 +13:00
Dakota Walsh
5f66020e42 fix(docker): container health alignment EE-5008 (#8553) 2023-03-01 12:48:41 +13:00
Chaim Lev-Ari
b3e72ecaa0 fix(gitops): load correct auth creds [EE-4849] (#8550)
close [EE-4849]
2023-02-28 17:55:54 +02:00
Chaim Lev-Ari
b98c71f1ab refactor(ui): move react components to react codebase [EE-3354] (#8258)
* refactor(ui): move react components to react codebase [EE-3354]

* refactor(app): move bocx selector options

* refactor(react): spearate portainer components

* fix(app): fix imports
2023-02-28 17:32:29 +02:00
matias-portainer
f9a09301a8 fix(edgejobs): fix data race in edge jobs tasks collect EE-4766 (#8542) 2023-02-28 12:14:09 -03:00
Chaim Lev-Ari
2c247efd0f fix(settings/oauth): show internal auth prompt by default [EE-4576] (#8481)
* fix(settings/oauth): show internal auth prompt by default [EE-4576]

fix [EE-4576]

* fix(oauth): use new confirm modal
2023-02-28 16:49:46 +02:00
Chaim Lev-Ari
86d0e30eb7 fix(build): ignore source maps for 3rd party [EE-5106] (#8549) 2023-02-28 14:54:52 +02:00
Chaim Lev-Ari
69a91ff90a fix(ui/box-selectors): make multi use square [EE-3856] (#8470) 2023-02-27 12:47:29 +02:00
Chaim Lev-Ari
e0481f69b1 chore(deps): replace semver compare with local solutions [EE-5018] (#8491) 2023-02-26 15:25:24 +02:00
matias-portainer
088262b6dc fix(edgejobs): fix data race on task logs clear EE-4767 (#8236) 2023-02-23 16:21:17 -03:00
Oscar Zhou
1b12ee9f01 fix(security): update dependency and binary version [EE-4863] (#8394) 2023-02-23 22:29:48 +13:00
Chaim Lev-Ari
5507b1e8c9 refactor(ui/editor): migrate code-editor to react [EE-4848] (#8257) 2023-02-23 09:10:31 +04:00
Chaim Lev-Ari
273a3f9a10 refactor(gitops): migrate git form to react [EE-4849] (#8268) 2023-02-23 01:43:33 +05:30
Oscar Zhou
afe6cd6df0 fix(code/security): vcs status error [EE-5062] (#8510) 2023-02-22 10:40:09 +13:00
andres-portainer
95ac2cc4c3 chore(edge): add transaction support for common objects EE-5079 (#8522) 2023-02-20 16:11:18 -03:00
Chaim Lev-Ari
9a8e95d017 feat(upgrade): show subtle banner [EE-5017] (#8489) 2023-02-19 09:47:50 +05:30
Oscar Zhou
631503fc1b fix(upgrade): add label to upgrade mustache template [EE-5029] (#8505) 2023-02-17 13:56:01 +13:00
Ali
23f3008500 chore(notifications):improve performance [EE-4815] (#8475)
* chore(notifications):improve performance [EE-4815]

Co-authored-by: testa113 <testa113>
2023-02-17 12:51:00 +13:00
matias-portainer
89dd72b4ac fix(registry): correct words in registry page and other places EE-4894 (#8356) 2023-02-16 10:31:08 -03:00
Chaim Lev-Ari
5a375ff055 refactor(environment/edge): show copy token button [EE-3691] (#8497) 2023-02-16 09:13:30 +05:30
andres-portainer
f081631808 fix(edgegroups): fix data-race in edgeGroupCreate EE-4435 (#8477) 2023-02-14 15:18:07 -03:00
Chaim Lev-Ari
e66dea44e3 refactor(ui/modals): replace bootbox with react solution [EE-4541] (#8010) 2023-02-14 13:49:41 +05:30
cmeng
392c7f74b8 fix(backup): reword region placeholder [EE-5012 (#8496) 2023-02-14 16:13:53 +13:00
Matt Hook
1dba5e464b minor fix (#8485) 2023-02-14 10:35:21 +13:00
Matt Hook
56dc2d1000 fix typo (#8486) 2023-02-13 23:23:44 +13:00
Ali
5c05ec489e fix(nameField): imperatively call setDebouncedValue when the value is changed by setFieldValue [EE-5002] (#8468)
Co-authored-by: testa113 <testa113>
2023-02-13 15:06:14 +13:00
Oscar Zhou
cef9255161 fix(snapshot): prevent snapshot containers from fast failing in Swarm mode (#8308) 2023-02-13 14:42:10 +13:00
Ali
0befdacc0e chore(prettier): ignore tailwind changes [EE-4809] (#8483)
Co-authored-by: testa113 <testa113>
2023-02-13 11:38:06 +13:00
Matt Hook
b2105f3614 feat(dockerfile): layered images [EE-4879] (#8301)
* multistage build to target production and storybook

* missing slash

* add storybook for windows too and build targets

* feature flag the storybook api

* remove kompose and prep for new FF lib

* todo comment for FF

* update to new feature flags library

* simplify logic

* fix compilation error

* simplified it
2023-02-13 11:28:32 +13:00
Ali
58d66d3142 chore(prettier): add tailwind prettier plugin [EE-4809] (#8221)
* add prettier plugin

* apply tailwind prettier formatting
2023-02-13 10:04:24 +13:00
Ali
9f6702d0b8 feat(resourcequotas): reduce resource quota requests [EE-4757] (#8420) 2023-02-10 18:28:53 +13:00
Matt Hook
44d69f3a3f hide the password in the response (#8437) 2023-02-10 18:26:18 +13:00
Matt Hook
e255bd710f chore(golang) remove api pkg and go mod tidy (#8474)
* remove this.  It exists and should be in the top level directory

* go mod tidy
2023-02-10 16:48:53 +13:00
Prabhat Khera
d73622ed9c fix cursor on autofill (#8378) 2023-02-10 09:19:43 +13:00
andres-portainer
4753d52532 fix(tls): specify the TLS MinVersion always EE-4427 (#7869) 2023-02-09 16:13:35 -03:00
Matt Hook
f9bbe000fb fix(docker): remove prepended slash by default on container names [EE-3592] (#8195)
* remove prepended slash by default if present

* trimcontainername still needed
2023-02-09 17:18:19 +13:00
Matt Hook
bfc610c192 feat(featureflags): improved feature flag handling [EE-4609] (#8222)
* updated and improved feature flags using new module

* merge init into parse

* update the package documentation

* better docs

* minor tidy
2023-02-09 17:17:46 +13:00
cmeng
51b9804fab fix(updater): specify docker client version [EE-5010] (#8459) 2023-02-08 17:00:22 +13:00
Matt Hook
e2168d21c7 guard around kube actions for endpoint inspect (#8430) 2023-02-07 23:13:52 +13:00
Chaim Lev-Ari
2dddc1c6b9 refactor(ui/box-selector): replace all selectors [EE-3856] (#7902) 2023-02-07 09:03:57 +05:30
Ali
c9253319d9 fix(texttip): fix texttip in edge groups [EE-4990] (#8441)
Co-authored-by: testa113 <testa113>
2023-02-03 13:39:45 +13:00
Chaim Lev-Ari
968fc98401 fix(home): show tooltip for disabled envs [EE-4859] (#8283) 2023-02-02 18:24:00 -03:00
LP B
921e9cfc6e fix(app/edge): updater/rollback calendar visual issues (#8386) 2023-02-02 12:57:53 -03:00
LP B
9b53960906 fix(api/edgestack): update deployments count when env relations are updated (#8433) 2023-02-02 12:04:58 -03:00
Oscar Zhou
402a62a5e2 fix(schedule): update date picker after removing edge group [EE-4963] (#8418) 2023-02-02 11:14:48 +13:00
cmeng
3470ea049a fix(update): prevent formik reinitialize [EE-4962] (#8426) 2023-02-02 09:59:17 +13:00
Ali
7fd263e8cc fix(texttip): fix texttip placement [EE-4990] (#8427)
Co-authored-by: testa113 <testa113>
2023-02-01 20:34:04 +13:00
Chaim Lev-Ari
36c6d3f21b fix(home): reduce update text for small screens [EE-4936] (#8422) 2023-02-01 12:14:53 +05:30
andres-portainer
5f3dd0a64f fix(edgestacks): fix edge stacks cache invalidation EE-4909 (#8399) 2023-02-01 02:16:04 -03:00
cmeng
42ca1287df fix(edge/stack): not clear stack status if version not updated [EE-4970] (#8408) 2023-02-01 09:18:04 +13:00
matias-portainer
2874a79279 fix(doc): update endpoint creation swagger documentation EE-4925 (#8415) 2023-01-31 11:06:27 -03:00
Ali
8574dd2371 fix(edge stacks): allow viewing existing kompose stacks [EE-4967] (#8405)
Co-authored-by: testa113 <testa113>
Co-authored-by: Matt Hook <hookenz@gmail.com>
2023-01-31 10:03:21 +13:00
Dakota Walsh
53eb5aa1ee fix(kube): 30 second delay to storage detection EE-4822 (#8360) 2023-01-31 09:58:57 +13:00
cmeng
eb8644330e fix(edge/job): init endpoints if it is null [EE-4972] (#8411) 2023-01-27 22:08:13 +13:00
cmeng
8663de580a fix(settings): EE-4959 Cannot turn on Edge Compute Features on CE (#8396) 2023-01-27 17:04:40 +13:00
Oscar Zhou
34298d96c5 fix: pass endpoint entity instead of endpoint.id (#8407) 2023-01-27 12:41:54 +13:00
cmeng
9d103ffbeb fix(UI): EE-4937 low resolution hides add container button (#8401) 2023-01-27 09:18:48 +13:00
Chaim Lev-Ari
5847c2b8ef fix(system/update): submit license form [EE-4743] (#8381) 2023-01-26 20:35:04 +05:30
matias-portainer
a09fe7e10c chore(edgejobs): AddEdgeJob disregards async mode EE-4855 (#8287) 2023-01-26 11:32:11 -03:00
Ali
5640cce4d6 chore(kompose): remove from settings [EE-4741] (#8375) 2023-01-26 16:03:44 +13:00
Chaim Lev-Ari
00bbf4ac63 refactor(auth): cache user data [EE-4935] (#8380) 2023-01-26 07:40:05 +05:30
matias-portainer
a748e15c16 feat(ui): bump codemirror version EE-4892 (#8393) 2023-01-25 10:31:12 -03:00
matias-portainer
cfdb9c126f fix(endpoints): check environment type before start metrics detection EE-4944 (#8391) 2023-01-25 10:29:11 -03:00
Chaim Lev-Ari
851a3346a9 fix(edge/update): remove schedule date for old envs [EE-3023] (#8315) 2023-01-24 12:20:55 +05:30
Oscar Zhou
c9aae27b29 fix(ci/security): update the node and golang version (#8387) 2023-01-24 14:41:00 +13:00
Prabhat Khera
087848539f fix(kubernetes): detect metrics API for kubernetes endspoints EE-4865 (#8351) 2023-01-24 09:05:15 +13:00
LP B
a74e389521 chore(dependencies): upgrade msw to fix xmldom CVE (#8362)
* chore(dependencies): upgrade msw to fix xmldom CVE

* refactor(msw): rename msw DefaultRequestBody to DefaultBodyType
2023-01-23 14:03:11 +01:00
Chaim Lev-Ari
eff6ec9df9 fix(edge/group): show tag selector when no tags [EE-4924] (#8368) 2023-01-23 11:05:23 +05:30
Prabhat Khera
8dec95c2cd chore(lint): add golangci linter to GitHub workflow EE-4872 (#8366) 2023-01-23 18:31:15 +13:00
Ali
5b02f636d7 fix(namespace): move warning [EE-4885] (#8370)
Co-authored-by: testa113 <testa113>
2023-01-23 10:41:39 +13:00
Prabhat Khera
ac458d0daa fix(ui): tooltip link color EE-4914 (#8365) 2023-01-23 10:15:47 +13:00
Chaim Lev-Ari
5b5dc320d5 fix(settings): save only the existing values [EE-4903] (#8326) 2023-01-23 02:34:39 +05:30
cmeng
d04747b309 fix(home): EE-4874 homepage ui issues (#8319) 2023-01-23 09:14:06 +13:00
LP B
07dd6bbe84 fix(home): dont display disconnected status similar to disabled (#8311) 2023-01-20 17:34:16 +01:00
Chaim Lev-Ari
406ff8812c feat(system/upgrade): add get license dialog [EE-4743] (#8249) 2023-01-19 15:31:49 +02:00
Chaim Lev-Ari
5942f4ff58 fix(home): dark mode for edit buttons [EE-4828] (#8241) 2023-01-19 10:16:34 +02:00
Prabhat Khera
adf92ce5e0 fix golang ci linter prehook (#8359) 2023-01-19 12:11:09 +13:00
cmeng
fed3d14adf fix(home): EE-4906 home page tiles for edge devices have the wrong url (#8353) 2023-01-19 09:00:43 +13:00
Prabhat Khera
73db588080 chore(linting): configure go linter EE-4871 (#8288) 2023-01-18 15:20:42 +13:00
Ali
6769326c8b fix(ingress): update label [EE-4902] (#8330)
Co-authored-by: testa113 <testa113>
2023-01-18 13:29:59 +13:00
Ali
e6d0e297dd fix(app): update redeploy wording [EE-4889] (#8317)
Co-authored-by: testa113 <testa113>
2023-01-18 08:30:06 +13:00
matias-portainer
0cd272211a fix(edgejobs): fix edge jobs log collection EE-4893 (#8328) 2023-01-17 14:21:13 -03:00
matias-portainer
6570f1f8eb fix(edgejobs): remove endpoint from edge job mapping on endpoint deletion EE-4764 (#8212) 2023-01-17 09:47:23 -03:00
Chaim Lev-Ari
1c180346e4 fix(ldap): sync user teams when needed [EE-4802] (#8235) 2023-01-16 10:41:32 +02:00
Chaim Lev-Ari
1d5d1bb12d fix(home): enable kubeconfig button [EE-4833] (#8233) 2023-01-15 11:32:38 +02:00
cmeng
0c27316034 fix(UI) EE-4883 stack repository method console error (#8304)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2023-01-13 16:07:52 +13:00
Chaim Lev-Ari
d3bed3072b fix(home): link to correct env route [EE-4877] (#8295) 2023-01-12 15:02:37 +02:00
Chaim Lev-Ari
329e8bcad5 fix(home): allow non-admins [EE-4873] (#8297) 2023-01-12 15:01:52 +02:00
Chaim Lev-Ari
4bdf30c038 feat(home): remove margins from env list [EE-4868] (#8285) 2023-01-11 10:59:56 +02:00
Matt Hook
7793b98813 straight not back ticks (#8292) 2023-01-11 14:54:50 +13:00
Matt Hook
02de7b2715 feat(version): add version setting script bump version EE-2529 (#6820)
* version bumper

* tell the user to check update
2023-01-11 14:07:18 +13:00
Chaim Lev-Ari
9c0e0607a4 fix(sidebar): put behind modal [EE-4866] (#8282)
* fix(sidebar): put behind modal

* fix(system/upgrade): validate on open dialog
2023-01-11 08:32:03 +13:00
Chaim Lev-Ari
baf9c3db0a feat(environments): add edge device [EE-4840] (#8246)
* feat(environments): add edge device [EE-4840]

fix [EE-4840]

* fix(home): fix tests
2023-01-11 08:30:49 +13:00
Prabhat Khera
6c193a8a45 refactor(log): log.fatal usage review EE-4607 (#8280) 2023-01-10 15:12:24 +13:00
Dakota Walsh
48a0f40621 fix(padding): fix margin-right on DatatableFooter pagination EE-4831 (#8237) 2023-01-10 10:14:46 +13:00
Matt Hook
4dc643acd9 fix(rolling-restart): wording and icon changes [EE-4834] (#8239)
* icon and wording changes

* fix inconsistencies and grammar

* fix(ui/buttons): show tooltip

* Change icon and confirmation dialog

* edit icon

* rename be-only-button to be-teaser-button for consistency

Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portainer.io>
2023-01-10 10:02:07 +13:00
Ali
1d42db93f1 fix(rbac): fix false negative rbac result in github microk8s environments [EE-4829] 2023-01-09 17:55:28 +13:00
andres-portainer
33c3f8460c Bump to v2.18.0. (#8266) 2023-01-07 12:08:23 -03:00
andres-portainer
dd0d1737b0 fix(performance): optimize performance for edge EE-3311 (#8040) 2023-01-06 16:25:41 -03:00
andres-portainer
3d28a6f877 chore(edgejobs): clean up EE-4850 (#8259) 2023-01-03 13:12:12 -03:00
andres-portainer
2fc518f221 chore(edgestacks): clean up EE-4851 (#8260) 2023-01-03 10:49:29 -03:00
andres-portainer
137ce37096 chore(nil): remove unnecessary nil checks EE-4847 (#8254) 2022-12-30 15:26:46 -03:00
andres-portainer
e529327851 chore(utils): remove dead code EE-4846 (#8253) 2022-12-30 14:52:58 -03:00
andres-portainer
3625ab6faa fix(rand): seed the RNG EE-4845 (#8252) 2022-12-30 14:52:18 -03:00
Chaim Lev-Ari
afb024d2a4 chore(docs): fix api reference [EE-4835] (#8242) 2022-12-25 10:47:21 +02:00
andres-portainer
b2bc4b92d6 fix(ui): remove extra margins from Dockerfile details section EE-4790 (#8213) 2022-12-21 19:09:40 -03:00
Chaim Lev-Ari
e5fd0c9595 fix(system): ignore failure to connect to docker [EE-4825] (#8231) 2022-12-21 18:08:18 +02:00
Chaim Lev-Ari
649c1c9cee feat(system): check BE image existence before upgrade [EE-4071] (#8230) 2022-12-21 17:17:51 +02:00
cmeng
919a854d93 feat(edge): EE-4570 allow pre-pull images with edge stack deployment (#8210)
Co-authored-by: Matt Hook <hookenz@gmail.com>
2022-12-21 13:18:51 +13:00
Chaim Lev-Ari
7fe0712b61 feat(home): move edge device to view [EE-4559] (#8189)
Co-authored-by: matias.spinarolli <matias.spinarolli@portainer.io>
2022-12-21 10:07:34 +13:00
Chaim Lev-Ari
b4a6f6911c fix(home): wrap items on small screens [EE-4798] (#8220) 2022-12-20 10:57:59 +02:00
Chaim Lev-Ari
59d35d26d8 feat(home): open env on click [EE-4792] (#8219) 2022-12-20 10:57:25 +02:00
Dakota Walsh
95558ed4ad fix(EE-4782): add portainer internal label to created ingress rules (#8196) 2022-12-20 16:46:51 +13:00
matias-portainer
e1b474d04f feat(edgejobs): support edge groups when using edge jobs EE-3873 (#8099) 2022-12-19 18:54:51 -03:00
Matt Hook
9732d1b5d8 remove kubeshell pod constraint (#8185) 2022-12-20 10:32:49 +13:00
Chaim Lev-Ari
701410d259 feat(app): clear env when log out [EE-4791] (#8218) 2022-12-19 08:56:39 +02:00
Chaim Lev-Ari
123754cee7 feat(system): make ui fixes to upgrade dialog [EE-4071] (#8199)
* feat(system): make ui fixes to upgrade dialog

* fix(system): hide on non-enabled platforms
2022-12-18 19:57:41 +02:00
Chaim Lev-Ari
d75d2ba9ce fix(docker): fix links to docs [EE-4486] (#8166) 2022-12-18 10:13:18 +02:00
Dakota Walsh
046738c967 feat(kubernetes): cluster setup reasonable defaults EE-4518 (#8082) 2022-12-16 16:03:40 +13:00
Matt Hook
0436be7bc4 update pull latest image wording (#8191) 2022-12-16 14:57:07 +13:00
cmeng
94d64997cc fix(compose): EE-4777 Toast Error displays insufficient details when creating stack in Standalone (#8208) 2022-12-16 14:43:24 +13:00
Chaim Lev-Ari
294d1668d4 feat(edge/updates): sync code changes from EE [EE-3023] (#8186)
* feat(edge/updates): add info about each field

* feat(edge/update): add beta feature

* fix(edge/update): add info
2022-12-16 14:20:22 +13:00
Chaim Lev-Ari
4bd6618fb9 chore(deps): upgrade docker binary [EE-4545] (#8146)
closes [EE-4545]
upgrades docker to v20.10.21

Co-authored-by: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
2022-12-16 14:07:21 +13:00
Chaim Lev-Ari
62197a67f7 chore(deps): upgrade docker compose [EE-4642] (#8147)
closes [EE-4642]
upgrades docker compose to v2.13.0
2022-12-16 14:05:14 +13:00
Matt Hook
c1dc1b49d1 fix(pagination): vert center pagination control on all screens and add consistent spacing [EE-4006] (#8192) 2022-12-16 10:55:54 +13:00
andres-portainer
b917e12b62 fix(fileservice): add missing interface EE-3458 (#8207) 2022-12-15 17:21:58 -03:00
andres-portainer
a8ccd2b153 feat(filestore): add function to save mTLS certificates (#8206) 2022-12-15 16:16:29 -03:00
LP B
68975620c5 fix(app): cursor jumps to the end of inputs [EE-4359] (#8163)
* feat(app/react): introduce controlled inputs HOC to avoid creating uncontrolled React components

* fix(app/env): jumping inputs when adding gpus to existing environment
2022-12-15 17:49:36 +01:00
LP B
67d3abcc9d fix(app/analytics): exclude login page in analytics (#8172) 2022-12-15 17:48:59 +01:00
matias-portainer
90b0cb84f4 fix(wording): change wording on CE teaser EE-4590 (#8136) 2022-12-15 11:25:13 -03:00
Oscar Zhou
b22cdb3559 fix(access/team): team member cannot change ownership to its own team [EE-4423] (#8052) 2022-12-15 21:27:18 +13:00
andres-portainer
37896661d6 fix(edgestacks): avoid a data race in edge stack status update endpoint EE-4737 (#8168) 2022-12-14 10:41:45 -03:00
Chaim Lev-Ari
f38b8234d9 fix(edge/groups): show tags for environments [EE-4422] (#7940) 2022-12-14 09:17:49 +02:00
Chaim Lev-Ari
52e150fa29 fix(notifications): break lines for long message [EE-4521] (#8110) 2022-12-14 09:17:12 +02:00
Matt Hook
929749c0da incorrect variable name used (#8198) 2022-12-14 17:49:20 +13:00
Ali
09bf5d03f4 give docker specific tooltip link (#8197)
Co-authored-by: testa113 <testa113>
2022-12-14 17:26:19 +13:00
Ali
ac6f52ab76 fix(gitapp): set manifest in git settings [EE-4734] (#8190) 2022-12-14 14:30:15 +13:00
Dakota Walsh
0ddcad66f3 fix(auth): invalidate session when permissions change EE-3320 (#8103) 2022-12-14 10:12:00 +13:00
Chaim Lev-Ari
930d9e5628 feat(edge/stacks): use namespace in manifest [EE-4507] (#8145) 2022-12-13 22:56:47 +02:00
Chaim Lev-Ari
8936ae9b7a feat(home): add connect and browse buttons [EE-4182] (#8175) 2022-12-13 22:56:09 +02:00
Chaim Lev-Ari
db9d87c918 feat(system): path to upgrade swarm to BE [EE-4624] (#8124) 2022-12-13 22:52:06 +02:00
matias-portainer
b59a0ba823 fix(settings): fix settings save/update EE-3604 (#8180) 2022-12-12 15:11:12 -03:00
Matt Hook
2188005b48 feat(kubernetes): add rolling restart button teaser [EE-4510] (#8126)
* rolling restart teaser button

* add be only rolling restart

* move position of button
2022-12-12 14:30:05 +13:00
Prabhat Khera
a1528475ba feat(UI): tooltip html message support and width fix [EE-3445] (#8165)
Co-authored-by: testa113 <testa113>
2022-12-12 14:03:50 +13:00
Chaim Lev-Ari
5cbf52377d feat(system): path to upgrade standalone to BE [EE-4071] (#8095) 2022-12-11 08:58:22 +02:00
andres-portainer
756ac034ec fix(go): add Go workspace EE-4763 (#8182) 2022-12-09 13:23:01 -03:00
Oscar Zhou
1008afd1fe fix(session): reset the environment session storage after deleting it (#8119) 2022-12-09 11:11:24 +13:00
Ali
563ead85cc fix(gitoptions): git app edit ui tweaks [EE-4584] (#8159) 2022-12-09 10:41:11 +13:00
Chaim Lev-Ari
eba5879ec8 feat(home): change layout of env tile [EE-4479] (#8061) 2022-12-07 16:51:20 +02:00
matias-portainer
b48aa1274d fix(host): fix host info request EE-4641 (#8169) 2022-12-07 10:48:42 -03:00
Matt Hook
3e485c3152 feat(api): remove golang experimental packages [EE-3648] (#8081)
* remove golang experimental packages

* rebase and fix references
2022-12-07 17:15:52 +13:00
Oscar Zhou
dffd45c5f9 refactor(ui): extract TagButton from TagSelector component [EE-4194] (#8127) 2022-12-07 16:15:09 +13:00
Ali
c1cc8bad77 feat(rbac): detect if rbac is enabled [EE-4308] (#8139) 2022-12-07 15:53:06 +13:00
matias-portainer
8dcc5e4adb fix(edgestacks): fix repository edge stack creation EE-4732 (#8160) 2022-12-06 10:26:18 -03:00
Matt Hook
4558ce84cf fix(header): removed red dot from help menu [EE-4586] (#8133)
* remove red dot from help menu

* fix cursor, add menu-icon class back
2022-12-06 10:24:38 +13:00
Ali
adc87b8f8e feat(deployment): PO feedback [EE-4416] (#8143) 2022-12-06 08:49:26 +13:00
Chaim Lev-Ari
ce8455953e chore(deps): upgrade k8s client [EE-4543] (#8019) 2022-12-05 09:58:55 +02:00
Prabhat Khera
a61b18dd93 feat(kubernetes): fix wordings for ingresses toggle EE-2647 (#8151) 2022-12-05 10:47:56 +13:00
Matt Hook
d6a3fe23e9 feat(libhelm) update missed package paths [EE-4650] (#8134)
* add missing pkg paths

* fix go tests

* fixed pkg paths
2022-12-05 10:38:16 +13:00
Prabhat Khera
cbaba43842 fix(ui): tooltip stays open on hover [EE-3445] (#8051) 2022-12-05 09:47:43 +13:00
Matt Hook
c173888b64 remove old digest library and update path (#8141) 2022-12-02 10:46:59 +13:00
Chaim Lev-Ari
82e9e2a895 refactor(edge/updates): sync changes from EE [EE-4288] (#7726) 2022-12-01 08:40:52 +02:00
Matt Hook
4fee359247 feat(libhelm) import libhelm into CE pkg directory [EE-4650] (#8138)
* import libhelm and update some paths

* remove file

* update readme
2022-12-01 14:27:49 +13:00
Ali
9cdc0da615 feat(kompose): hide kompose [EE-4562] (#8084) 2022-12-01 13:46:23 +13:00
Matt Hook
8fd0efa34f add third_party digest lib to top level (#8140) 2022-12-01 13:26:39 +13:00
Matt Hook
79bfd8f6fe fix package import for docker/distribution (#8132) 2022-12-01 09:43:51 +13:00
Ali
2114c15f55 refactor(data-cy): duplicate data cy [EE-4656] (#8131) 2022-11-30 16:54:28 +13:00
Matt Hook
d2f6d1e415 import libhelm into portainer (#8128) 2022-11-30 14:25:47 +13:00
Matt Hook
241440a474 fix(fdo): import deleted digest library [EE-4654] (#8129)
* import digest lib

* update references

* fix lint errors
2022-11-30 09:11:49 +13:00
LP B
2e19f4ea6d fix(docker/container): auto select private access when enabling UAC on public container edit (#8032) [EE-44-64] 2022-11-29 11:24:13 +01:00
Matt Hook
95bc508462 fix(migrator): fix setting version struct fields after migration [EE-4613] (#8090)
* fix setting version struct fields

* fix go tests

* remove versionUpdateRequired

* remove old code that was originally for debugging purposes
2022-11-28 19:28:10 +13:00
Ali
d78b762f7b refactor(icons): replace fa icons [EE-4459] (#7907)
refactor(icons): remove fontawesome EE-4459

refactor(icon) replace feather with lucide EE-4472
2022-11-28 15:00:28 +13:00
andres-portainer
9dfac98a26 fix(tags): add missing error handling EE-4622 (#8102) 2022-11-24 14:16:04 -03:00
andres-portainer
e26a607d28 fix(edgegroups): avoid a last-write-wins situation when updating edge groups concurrently EE-3732 (#8101) 2022-11-23 21:36:17 -03:00
Dakota Walsh
6dc1841c14 fix(kube): disable namespace resource quota by default EE-4421 (#8080) 2022-11-23 15:01:41 +13:00
andres-portainer
c28be7aced fix(token-cache-manager): refactor to avoid data races EE-4438 (#8094) 2022-11-22 18:31:14 -03:00
Prabhat Khera
dd01165224 show secrets in kube applications datatable (#8065) 2022-11-23 08:48:36 +13:00
Chaim Lev-Ari
d484a0eb64 fix(docker): remove word break in details [EE-4481] (#7996) 2022-11-22 15:00:55 +02:00
Chaim Lev-Ari
fe8e834dbf refactor(ui/datatables): migrate views to use datatable component [EE-4064] (#7609) 2022-11-22 14:16:34 +02:00
Prabhat Khera
0f0513c684 feat(kubernetes): edit yaml support EE-2855 (#8016) 2022-11-22 09:40:44 +13:00
Chaim Lev-Ari
7006c17ce4 fix(wizard): debounce name state [EE-4177] (#8042)
move debouncing to the component (from the validation).
debounce returns undefined when it's not calling the debounced function,
and undefined is considered a validation error.
2022-11-21 19:33:08 +02:00
Chaim Lev-Ari
253a3a2b40 fix(ui): remove loading of missing interceptors [EE-4604] (#8086) 2022-11-21 17:25:09 +02:00
Chaim Lev-Ari
1e4c4e2616 refactor(edge): move edge codebase to react (#7781) 2022-11-21 09:51:55 +02:00
Chaim Lev-Ari
75f40fe485 refactor(portainer): remove offline mode [EE-4348] (#7761)
Co-authored-by: deviantony <anthony.lapenna@portainer.io>
2022-11-21 09:50:47 +02:00
Ali
61e8e68c31 fix(svg): fix footer height [EE-4547] (#8071) 2022-11-21 09:22:41 +13:00
Matt Hook
583346321e feat(version): migrate version to semver [EE-3756] (#7693)
redisigned version bucket and migration code
2022-11-18 13:18:09 +13:00
cmeng
4cfa584c7c fix(git): EE-4577 Git Repository Fields are Missing in Edge Stacks (#8057) 2022-11-18 08:59:58 +13:00
Ali
d012a4efc4 feat(deployment): enforce deployment options EE-4416 (#7974) 2022-11-17 22:00:34 +13:00
Oscar Zhou
e0f3a8c0a2 fix(access/viwer): update the viwer filter key to user.id (#8055) 2022-11-17 10:38:34 +13:00
Yi Chen
bb48ab00cb * remove empty examples (#7952)
* fix missing type
2022-11-17 06:55:08 +13:00
Chaim Lev-Ari
eccc8131dd feat(system/upgrade): add upgrade banner [EE-4564] (#8046) 2022-11-16 18:38:39 +02:00
matias-portainer
c21921a08d fix(edgestacks): return 400 instead of 500 on edge stack create when an invalid payload is provided EE-4429 (#7880) 2022-11-15 17:40:56 -03:00
Prabhat Khera
573e05d1c7 feat(kubernetes): BE teasure added EE-2647 (#7927) 2022-11-15 20:57:01 +13:00
Prabhat Khera
246e351817 remove wrong checked-in files from parent dir (#8048) 2022-11-15 09:01:28 +13:00
Prabhat Khera
6775c7b6ec clean database models directory (#8026) 2022-11-14 08:33:57 +13:00
Chaim Lev-Ari
881e99df53 refactor(nomad): sync frontend with EE [EE-3353] (#7758) 2022-11-13 12:29:25 +02:00
Chaim Lev-Ari
78dcba614d refactor(portainer): move to react [EE-3350] (#7915) 2022-11-13 10:10:18 +02:00
matias-portainer
30e23ea5b4 fix(ui): tidy up Edge Devices configuration EE-3604 (#7797) 2022-11-10 16:13:20 -03:00
Oscar Zhou
e1e81731b9 fix(setting/auth): allow to save all auth method if custom ldap server is empty string (#7990) 2022-11-10 08:28:09 +13:00
Oscar Zhou
16377221f9 fix(stack): check if endpoint exists before checking the user permission on the endpoint (#7967) 2022-11-09 12:20:19 +13:00
Chaim Lev-Ari
a0237852ef fix: fix occurred typo [EE-4536] (#7994) 2022-11-08 07:30:37 +02:00
andres-portainer
193e0c7d6f fix(snapshots): remove snapshots when removing endpoints EE-4527 (#7973)
* fix(snapshots): remove snapshots when removing endpoints EE-4527

* Fix nil pointer dereference.

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2022-11-07 20:28:18 -03:00
Chaim Lev-Ari
77c29ff87e refactor(kubernetes): move react codebase [EE-3349] (#7953) 2022-11-07 08:03:11 +02:00
Chaim Lev-Ari
2868da296a fix(stack): validate original containers names [EE-4520] (#7978) 2022-11-06 10:40:33 +02:00
Chaim Lev-Ari
ff10588383 fix(edge): show select all checkbox [EE-4129] (#7948) 2022-11-06 09:33:22 +02:00
LP B
6b02d9a1e3 fix(app/logs): change pattern to detect double serialized JSON logs [EE-4525] (#7962)
* fix(app/logs): change pattern to detect double serialized JSON logs

* fix(app/logs): fallback to raw display when parsing fails + include timestamp for Zerolog logs
2022-11-04 13:58:18 +01:00
Ali
9f3d5185b0 fix(slider): use and update react slider EE-4522 (#7987) 2022-11-04 14:12:53 +13:00
congs
f94147b07b fix(registry): EE-4526 Registry Manage access link broken (#7975) 2022-11-04 12:10:42 +13:00
andres-portainer
49d02e0386 fix(db): remove a bucket overwrite EE-4424 (#7867) 2022-11-03 18:51:15 -03:00
Ali
e82d0cfbdb fix(apps): fix edit error EE-4529 (#7963) 2022-11-03 13:04:56 +13:00
Oscar Zhou
c8051b68d4 fix(auth/ldap): allow to save internal authentication if custom ldap server is not set [EE-3155] (#7959) 2022-11-03 10:06:56 +13:00
Chaim Lev-Ari
37d4a80769 refactor(environments): remove endpoints cache [DTD-100] (#6408) 2022-11-02 13:29:26 +02:00
Chaim Lev-Ari
9ef2e27aae fix(environments): debounce name validation [EE-4177] (#7889) 2022-11-02 12:44:31 +02:00
Oscar Zhou
9e1f80cf37 chore(ui/ldap): add texttip for group search configuration (#7944) 2022-11-02 11:31:44 +13:00
Dakota Walsh
459c95169a fix(ingresses): migrate to new allow/disallow format EE-4465 (#7893) 2022-11-02 11:17:32 +13:00
fhanportainer
5048f08b5f fix(box-selector): fixed incorrect wording for Private Box selector under UAC. (#7949) 2022-11-01 11:06:11 +13:00
fhanportainer
e785d1572e fix(web-editor): fixed web editor scroll bar. (#7941) 2022-10-31 11:03:50 +13:00
Rex Wang
95a4f83466 fix(docker): docker template UI bug fix [EE-4034] (#7912)
* EE-4034 fix(docker): docker template UI bug fix

* EE-4034 fix(docker): fix ui
2022-10-30 14:56:23 +08:00
Dmitry Salakhov
4edf232e41 fix: document edge endoint url requirement (#7735) [EE-3425] 2022-10-28 13:00:12 +13:00
Dmitry Salakhov
903cf284e7 fix(image): build image from file (#7929) [EE-4501] 2022-10-27 23:31:31 +13:00
Prabhat Khera
a550bfaedb fix showing namespaces for standard user (#7917) 2022-10-27 16:14:54 +13:00
Hao
446febb0f6 fix(image): hide button issues [EE-4166] (#7845)
* fix(image): hide button issues [EE-4166]
2022-10-25 15:02:59 +08:00
Oscar Zhou
cb9fe2606c fix(team): disable team leader setting when external auth sync is enabled [EE-3579] (#7852) 2022-10-25 14:39:24 +13:00
Dakota Walsh
55211ef00e fix(ingress): allow none controller type EE-4420 (#7883)
Co-authored-by: testA113 <alex.harris@portainer.io>
2022-10-25 09:41:30 +13:00
Chaim Lev-Ari
e48ceb15e9 refactor(environments): move environments ts code to react [EE-3443] (#7747) 2022-10-23 09:53:25 +03:00
Rex Wang
1b12cc9f31 EE-4376 fix(docker): fix malformed struct of template (#7803) 2022-10-21 16:29:18 +08:00
Hao
0365ed8e70 fix(docker): comfirm modal for removing secrets/networks/configs [EE-4211] (#7882)
* fix(ui): comfirm modal for removing secrets/networks/configs [EE-4211]

* fix(ui): comfirm modal for removing secrets/networks/configs [EE-4211]

* fix(ui): comfirm modal for removing secrets/networks/configs [EE-4211]
2022-10-21 16:03:41 +08:00
Chaim Lev-Ari
7624ff10ee chore(edge): add aria-label for edge-group selector [EE-4466] (#7896)
* chore(edge): add aria-label for edge-group selector

* style(edge): remove comment
2022-10-21 08:22:49 +03:00
andres-portainer
535a26412f fix(logging): default to pretty logging [EE-4371] (#7847)
* fix(logging): default to pretty logging EE-4371

* feat(app/logs): prettify stack traces in JSON logs

* feat(nomad/logs): prettify JSON logs in log viewer

* feat(kubernetes/logs): prettigy JSON logs in log viewers

* feat(app/logs): format and color zerolog prettified logs

* fix(app/logs): pre-parse logs when they are double serialized

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2022-10-20 16:33:54 +02:00
Chaim Lev-Ari
ee5600b6af chore(build): incremental ts build [EE-4204] (#7888) 2022-10-20 17:23:56 +03:00
Rex Wang
3f51d077ac fix(docker): create custom template [EE-4114] (#7812)
* EE-4114 fix(docker): create custom template

* Update customtemplate_create.go

remove white space
2022-10-20 16:42:49 +08:00
Rex Wang
0219d41ba7 fix(docker): Show stopped container on dashboard [EE-4327] (#7833)
* EE-4327 fix(docker): Show stopped container on dashboard

* Update ContainerStatus.tsx

remove comment

* EE-4327 fix(docker): show stopped container on dashboard
2022-10-20 15:10:39 +08:00
Prabhat Khera
f3e2ccd487 fix volume claims with k8s app (#7899) 2022-10-20 15:43:19 +13:00
Ali
368e6b2a44 fix(helm): helm charts view bad icon aspect ratio EE-4451 (#7875) 2022-10-20 14:14:55 +13:00
Dmitry Salakhov
1100a2bd28 feat: move jwt lib to v4 (#7773) 2022-10-20 10:26:11 +13:00
itsconquest
16dc66f173 fix(UAC): put team into resource control when editing as team lead [EE-4457] (#7886)
* fix(UAC): put team into resource control when editing as team lead [EE-4457]

* populate form values & payload correctly
2022-10-20 10:18:56 +13:00
itsconquest
c1f94be9b2 fix(notifications): sort by newest first by default [EE-4467] (#7891) 2022-10-19 15:25:20 +13:00
Matt Hook
58947fee69 fix(libhelm): new libhelm with relaxed validation when adding chart repo [EE-4440] (#7874)
update helm to fix adding some chart repos
2022-10-19 12:43:33 +13:00
Dakota Walsh
0c995ae1c8 fix(kubernetes): create proxied kubeclient EE-4326 (#7850) 2022-10-18 10:46:27 +13:00
itsconquest
f6d6be90e4 fix(UAC): provide required UI context [EE-4415] (#7854) 2022-10-18 09:45:47 +13:00
andres-portainer
5488389278 fix(code): replace calls to ioutil EE-4425 (#7878) 2022-10-17 15:29:12 -03:00
andres-portainer
69f498c431 fix(tests): add missing context cancel EE-4433 (#7879) 2022-10-17 13:57:41 -03:00
Prabhat Khera
669327da7c fix reloading page when ing class disallowed (#7830) 2022-10-17 10:44:17 +13:00
andres-portainer
191f8e17ee fix(code): remove unused code EE-4431 (#7866) 2022-10-14 19:42:31 -03:00
andres-portainer
ae2bec4bd9 fix(code): clean up EE-4432 (#7865) 2022-10-14 18:09:07 -03:00
andres-portainer
367f3dd6d4 fix(tags): remove a data race EE-4310 (#7862) 2022-10-13 11:12:12 -03:00
matias-portainer
8f1ac38963 fix(tags): get tags when loading associated endpoints selector EE-4140 (#7857) 2022-10-13 11:03:19 -03:00
Ali
7a6ff10268 fix(ing): nodeport validate and show errors [EE-4373] (#7801) 2022-10-12 10:06:57 +13:00
andres-portainer
fd91de3571 fix(logging): remove remaining traces of logrus EE-4414 (#7848) 2022-10-11 16:53:27 -03:00
andres-portainer
ab3a6f402e fix(build): add -trimpath EE-4406 (#7836) 2022-10-11 13:00:50 -03:00
andres-portainer
d3edb7ebd5 fix(logging): convert missing cases to Zerolog EE-4400 (#7817) 2022-10-11 12:59:00 -03:00
Oscar Zhou
c23b8b2816 fix(gitops): update the git ref cache key from url to url and pat (#7841) 2022-10-11 18:31:21 +13:00
itsconquest
724f1f63b7 fix(notifications): cleanup notifications code [EE-4274] (#7790)
* fix(notifications): cleanup notifications code [EE-4274]

* break long words
2022-10-11 14:05:53 +13:00
Ali
c6ae8467c0 fix(ingress): update ingress tls after deletion EE-4387 (#7804)
* fix(ing): update tls value EE-4387
2022-10-10 09:32:30 +13:00
Ali
56087bcbb3 fix(clustersetup): dont show modal when loading (#7810) 2022-10-08 17:48:36 +13:00
Ali
315c1c7e1e fix(application): edit cluster ip services EE-4328 (#7775) 2022-10-07 16:55:11 +13:00
congs
819dc4d561 fix(UI): EE-4381 environment ID is shown instead of its name when deleting an environment (#7808) 2022-10-07 16:36:19 +13:00
congs
380a64d546 fix(wizard): EE-4350 Environment creating script should only showed for relevant type of environment (#7786) 2022-10-07 15:43:06 +13:00
congs
6429546462 fix(help): EE-4335 context sensitive help improvement (#7754) 2022-10-07 14:25:26 +13:00
matias-portainer
ebfb71da05 fix(edge): fix docker proxy EE-4380 (#7799) 2022-10-06 11:12:39 -03:00
Ali
ae0b9b1e30 fix(ingress): ingress indicate missing services EE-4358 (#7794) 2022-10-06 15:24:59 +13:00
Oscar Zhou
e9de484c3e refactor(stack): stack build process backend only [EE-4342] (#7750) 2022-10-05 22:33:59 +13:00
1941 changed files with 42428 additions and 25055 deletions

View File

@@ -83,6 +83,7 @@ overrides:
'newlines-between': 'always',
},
]
no-plusplus: off
func-style: [error, 'declaration']
import/prefer-default-export: off
no-use-before-define: ['error', { functions: false }]
@@ -99,12 +100,17 @@ overrides:
'@typescript-eslint/explicit-module-boundary-types': off
'@typescript-eslint/no-unused-vars': 'error'
'@typescript-eslint/no-explicit-any': 'error'
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either' }]
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either', controlComponents: ['Input', 'Checkbox'] }]
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
'react/jsx-no-bind': off
'no-await-in-loop': 'off'
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
'regex/invalid': ['error', [{ 'regex': 'data-feather="(.*)"', 'message': 'Please use `react-feather` package instead' }]]
'regex/invalid': ['error', [{ 'regex': '<Icon icon="(.*)"', 'message': 'Please directly import the `lucide-react` icon instead of using the string' }]]
overrides: # allow props spreading for hoc files
- files:
- app/**/with*.ts{,x}
rules:
'react/jsx-props-no-spreading': off
- files:
- app/**/*.test.*
extends:

View File

@@ -2,4 +2,7 @@
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169
# tailwind prettier
58d66d3142950bb90a7d85511c034ac9fabba9ba

View File

@@ -23,6 +23,9 @@ jobs:
with:
node-version: '14'
cache: 'yarn'
- uses: actions/setup-go@v3
with:
go-version: 1.19.4
- run: yarn --frozen-lockfile
- name: Run linters
@@ -36,3 +39,9 @@ jobs:
gofmt_dir: api/
- name: Typecheck
uses: icrawl/action-tsc@v1
- name: GolangCI-Lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
working-directory: api
args: -c .golangci.yaml

View File

@@ -2,21 +2,22 @@ name: Nightly Code Security Scan
on:
schedule:
- cron: '0 8 * * *'
- cron: '0 20 * * *'
workflow_dispatch:
jobs:
client-dependencies:
name: Client dependency check
name: Client Dependency Check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
js: ${{ steps.set-matrix.outputs.js_result }}
steps:
- uses: actions/checkout@master
- name: checkout repository
uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
@@ -24,141 +25,115 @@ jobs:
with:
json: true
- name: Upload js security scan result as artifact
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/js-result")
- name: Upload js result html file
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-js-result-${{github.run_id}}
path: js-result.html
- name: Analyse the js result
- name: analyse vulnerabilities
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=js_result::${result}"
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
echo "js_result=${result}" >> $GITHUB_OUTPUT
server-dependencies:
name: Server dependency check
name: Server Dependency Check
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
go: ${{ steps.set-matrix.outputs.go_result }}
steps:
- uses: actions/checkout@master
- name: checkout repository
uses: actions/checkout@master
- name: Download go modules
- name: install Go
uses: actions/setup-go@v3
with:
go-version: '1.19.5'
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/golang@master
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=./api/go.mod
json: true
run: |
yarn global add snyk
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: Upload go security scan result as artifact
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=table --export --export-filename="/data/go-result")
- name: Upload go result html file
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-go-result-${{github.run_id}}
path: go-result.html
- name: Analyse the go result
- name: analyse vulnerabilities
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=go_result::${result}"
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=snyk --path="/data/snyk.json" --output-type=matrix)
echo "go_result=${result}" >> $GITHUB_OUTPUT
image-vulnerability:
name: Build docker image and Image vulnerability check
name: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
outputs:
image: ${{ steps.set-matrix.outputs.image_result }}
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Use golang 1.18
uses: actions/setup-go@v3
with:
go-version: '1.18'
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install packages and build
run: yarn install && yarn build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: build/linux/Dockerfile
tags: trivy-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
- name: Load docker image
run: |
docker load --input /tmp/trivy-portainer-image.tar
- name: Run Trivy vulnerability scanner
- name: scan vulnerabilities by Trivy
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress portainerci/portainer:develop
- name: Upload image security scan result as artifact
- name: upload image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-develop-result
path: image-trivy.json
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
- name: develop scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=table --export --export-filename="/data/image-result")
- name: Upload go result html file
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-${{github.run_id}}
path: image-result.html
- name: Analyse the trivy result
- name: analyse vulnerabilities
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
echo "::set-output name=image_result::${result}"
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=trivy --path="/data/image-trivy.json" --output-type=matrix)
echo "image_result=${result}" >> $GITHUB_OUTPUT
result-analysis:
name: Analyse scan result
name: Analyse Scan Results
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
@@ -169,21 +144,21 @@ jobs:
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
image: ${{fromJson(needs.image-vulnerability.outputs.image)}}
steps:
- name: Display the results of js, go and image
- name: display the results of js, Go, and image scan
run: |
echo ${{ matrix.js.status }}
echo ${{ matrix.go.status }}
echo ${{ matrix.image.status }}
echo ${{ matrix.js.summary }}
echo ${{ matrix.go.summary }}
echo ${{ matrix.image.summary }}
echo "${{ matrix.js.status }}"
echo "${{ matrix.go.status }}"
echo "${{ matrix.image.status }}"
echo "${{ matrix.js.summary }}"
echo "${{ matrix.go.summary }}"
echo "${{ matrix.image.summary }}"
- name: Send Slack message
- name: send message to Slack
if: >-
matrix.js.status == 'failure' ||
matrix.go.status == 'failure' ||
matrix.image.status == 'failure'
uses: slackapi/slack-github-action@v1.18.0
uses: slackapi/slack-github-action@v1.23.0
with:
payload: |
{

View File

@@ -12,10 +12,11 @@ on:
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
- '.github/workflows/pr-security.yml'
jobs:
client-dependencies:
name: Client dependency check
name: Client Dependency Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
@@ -23,9 +24,10 @@ jobs:
outputs:
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
steps:
- uses: actions/checkout@master
- name: checkout repository
uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
@@ -33,13 +35,13 @@ jobs:
with:
json: true
- name: Upload js security scan result as artifact
- name: upload scan result as pull-request artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-feat-result
path: snyk.json
- name: Download artifacts from develop branch
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
@@ -51,24 +53,24 @@ jobs:
echo "null" > ./js-snyk-develop.json
fi
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="/data/js-snyk-develop.json" -output-type=table -export -export-filename="/data/js-result")
- name: pr vs develop scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=table --export --export-filename="/data/js-result")
- name: Upload js result html file
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-js-result-compare-to-develop-${{github.run_id}}
path: js-result.html
- name: Analyse the js diff result
- name: analyse different vulnerabilities against develop branch
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
echo "::set-output name=js_diff_result::${result}"
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/js-snyk-feature.json" --compare-to="/data/js-snyk-develop.json" --output-type=matrix)
echo "js_diff_result=${result}" >> $GITHUB_OUTPUT
server-dependencies:
name: Server dependency check
name: Server Dependency Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
@@ -76,27 +78,32 @@ jobs:
outputs:
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
steps:
- uses: actions/checkout@master
- name: checkout repository
uses: actions/checkout@master
- name: Download go modules
- name: install Go
uses: actions/setup-go@v3
with:
go-version: '1.19.5'
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/golang@master
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=./api/go.mod
json: true
run: |
yarn global add snyk
snyk test --file=./api/go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: Upload go security scan result as artifact
- name: upload scan result as pull-request artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-feature-result
path: snyk.json
- name: Download artifacts from develop branch
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
@@ -108,24 +115,24 @@ jobs:
echo "null" > ./go-snyk-develop.json
fi
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=table -export -export-filename="/data/go-result")
- name: pr vs develop scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=table --export --export-filename="/data/go-result")
- name: Upload go result html file
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-go-result-compare-to-develop-${{github.run_id}}
path: go-result.html
- name: Analyse the go diff result
- name: analyse different vulnerabilities against develop branch
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
echo "::set-output name=go_diff_result::${result}"
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=snyk --path="/data/go-snyk-feature.json" --compare-to="/data/go-snyk-develop.json" --output-type=matrix)
echo "go_diff_result=${result}" >> $GITHUB_OUTPUT
image-vulnerability:
name: Build docker image and Image vulnerability check
name: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
@@ -133,50 +140,50 @@ jobs:
outputs:
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
steps:
- name: Checkout code
- name: checkout code
uses: actions/checkout@master
- name: Use golang 1.18
- name: install Go 1.19.5
uses: actions/setup-go@v3
with:
go-version: '1.18'
go-version: '1.19.5'
- name: Use Node.js 12.x
uses: actions/setup-node@v1
- name: install Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 12.x
node-version: 18.x
- name: Install packages and build
- name: install packages and build binary
run: yarn install && yarn build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: set up docker buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
uses: docker/build-push-action@v2
- name: build and compress image
uses: docker/build-push-action@v4
with:
context: .
file: build/linux/Dockerfile
tags: trivy-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
- name: Load docker image
- name: load docker image
run: |
docker load --input /tmp/trivy-portainer-image.tar
- name: Run Trivy vulnerability scanner
- name: scan vulnerabilities by Trivy
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
- name: Upload image security scan result as artifact
- name: upload image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-feature-result
path: image-trivy.json
- name: Download artifacts from develop branch
- name: download artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
@@ -188,24 +195,24 @@ jobs:
echo "null" > ./image-trivy-develop.json
fi
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="/data/image-trivy-develop.json" -output-type=table -export -export-filename="/data/image-result")
- name: pr vs develop scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=table --export --export-filename="/data/image-result")
- name: Upload image result html file
- name: upload html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-compare-to-develop-${{github.run_id}}
path: image-result.html
- name: Analyse the image diff result
- name: analyse different vulnerabilities against develop branch
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
echo "::set-output name=image_diff_result::${result}"
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=trivy --path="/data/image-trivy-feature.json" --compare-to="/data/image-trivy-develop.json" --output-type=matrix)
echo "image_diff_result=${result}" >> $GITHUB_OUTPUT
result-analysis:
name: Analyse scan result compared to develop
name: Analyse Scan Result Against develop Branch
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
@@ -217,17 +224,16 @@ jobs:
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
steps:
- name: Check job status of diff result
- name: check job status of diff result
if: >-
matrix.jsdiff.status == 'failure' ||
matrix.godiff.status == 'failure' ||
matrix.imagediff.status == 'failure'
run: |
echo ${{ matrix.jsdiff.status }}
echo ${{ matrix.godiff.status }}
echo ${{ matrix.imagediff.status }}
echo ${{ matrix.jsdiff.summary }}
echo ${{ matrix.godiff.summary }}
echo ${{ matrix.imagediff.summary }}
echo "${{ matrix.jsdiff.status }}"
echo "${{ matrix.godiff.status }}"
echo "${{ matrix.imagediff.status }}"
echo "${{ matrix.jsdiff.summary }}"
echo "${{ matrix.godiff.summary }}"
echo "${{ matrix.imagediff.summary }}"
exit 1

View File

@@ -29,6 +29,23 @@ module.exports = {
extensions: config.resolve.extensions,
}),
];
const svgRule = config.module.rules.find((rule) => rule.test && typeof rule.test.test === 'function' && rule.test.test('.svg'));
svgRule.test = new RegExp(svgRule.test.source.replace('svg|', ''));
config.module.rules.unshift({
test: /\.svg$/i,
type: 'asset',
resourceQuery: { not: [/c/] }, // exclude react component if *.svg?url
});
config.module.rules.unshift({
test: /\.svg$/i,
issuer: /\.(js|ts)(x)?$/,
resourceQuery: /c/, // *.svg?c
use: [{ loader: '@svgr/webpack', options: { icon: true } }],
});
return config;
},
core: {

View File

@@ -25,7 +25,7 @@ Each commit message should include a **type**, a **scope** and a **subject**:
<type>(<scope>): <subject>
```
Lines should not exceed 100 characters. This allows the message to be easier to read on github as well as in various git tools and produces a nice, neat commit log ie:
Lines should not exceed 100 characters. This allows the message to be easier to read on GitHub as well as in various git tools and produces a nice, neat commit log ie:
```
#271 feat(containers): add exposed ports in the containers view
@@ -63,7 +63,7 @@ The subject contains succinct description of the change:
## Contribution process
Our contribution process is described below. Some of the steps can be visualized inside Github via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
Our contribution process is described below. Some of the steps can be visualized inside GitHub via specific `status/` labels, such as `status/1-functional-review` or `status/2-technical-review`.
### Bug report
@@ -93,7 +93,7 @@ $ yarn start
Portainer can now be accessed at <https://localhost:9443>.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
Find more detailed steps at <https://docs.portainer.io/contribute/build>.
### Build customisation
@@ -103,6 +103,10 @@ You can customise the following settings:
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
## Testing your build
The `--log-level=DEBUG` flag can be passed to the Portainer container in order to provide additional debug output which may be useful when troubleshooting your builds. Please note that this flag was originally intended for internal use and as such the format, functionality and output may change between releases without warning.
## Adding api docs
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:

26
api/.golangci.yaml Normal file
View File

@@ -0,0 +1,26 @@
linters:
# Disable all linters.
disable-all: true
enable:
- depguard
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"
# Create additional guards that follow the same configuration pattern.
# Results from all guards are aggregated together.
# additional-guards:
# - list-type: allowlist
# include-go-root: false
# packages:
# - github.com/sirupsen/logrus
# # Specify rules by which the linter ignores certain files for consideration.
# ignore-file-rules:
# - "!**/*_test.go"

View File

@@ -21,7 +21,7 @@ type Monitor struct {
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
mu sync.Mutex
mu sync.RWMutex
adminInitDisabled bool
}
@@ -54,7 +54,8 @@ func (m *Monitor) Start() {
case <-time.After(m.timeout):
initialized, err := m.WasInitialized()
if err != nil {
log.Fatal().Err(err).Msg("")
log.Error().Err(err).Msg("AdminMonitor failed to determine if Portainer is Initialized")
return
}
if !initialized {
@@ -82,6 +83,7 @@ func (m *Monitor) Stop() {
if m.cancellationFunc == nil {
return
}
m.cancellationFunc()
m.cancellationFunc = nil
}
@@ -92,12 +94,14 @@ func (m *Monitor) WasInitialized() (bool, error) {
if err != nil {
return false, err
}
return len(users) > 0, nil
}
func (m *Monitor) WasInstanceDisabled() bool {
m.mu.Lock()
defer m.mu.Unlock()
m.mu.RLock()
defer m.mu.RUnlock()
return m.adminInitDisabled
}

View File

@@ -4,6 +4,7 @@ import (
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
@@ -42,7 +43,9 @@ func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (port
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)

View File

@@ -2,7 +2,6 @@ package archive
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
@@ -28,10 +27,10 @@ func listFiles(dir string) []string {
func Test_shouldCreateArhive(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
@@ -48,7 +47,7 @@ func Test_shouldCreateArhive(t *testing.T) {
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
copyContent, _ := os.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
@@ -60,10 +59,10 @@ func Test_shouldCreateArhive(t *testing.T) {
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
ioutil.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
ioutil.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
@@ -80,7 +79,7 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
wasExtracted := func(p string) {
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := ioutil.ReadFile(fullpath)
copyContent, _ := os.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}

View File

@@ -4,12 +4,12 @@ import (
"archive/zip"
"bytes"
"fmt"
"github.com/pkg/errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
)
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
@@ -36,7 +36,7 @@ func extractFileFromArchive(file *zip.File, dest string) error {
}
defer f.Close()
data, err := ioutil.ReadAll(f)
data, err := io.ReadAll(f)
if err != nil {
return err
}

View File

@@ -1,9 +1,10 @@
package archive
import (
"github.com/stretchr/testify/assert"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUnzipFile(t *testing.T) {

View File

@@ -3,8 +3,10 @@ package backup
import (
"context"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"time"
"github.com/pkg/errors"
@@ -43,6 +45,12 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
return errors.Wrap(err, "Failed to stop db")
}
// At some point, backups were created containing a subdirectory, now we need to handle both
restorePath, err = getRestoreSourcePath(restorePath)
if err != nil {
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
}
if err = restoreFiles(restorePath, filestorePath); err != nil {
return errors.Wrap(err, "failed to restore the system state")
}
@@ -59,6 +67,26 @@ func extractArchive(r io.Reader, destinationDirPath string) error {
return archive.ExtractTarGz(r, destinationDirPath)
}
func getRestoreSourcePath(dir string) (string, error) {
// find portainer.db or portainer.edb file. Return the parent directory
var portainerdbRegex = regexp.MustCompile(`^portainer.e?db$`)
backupDirPath := dir
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if portainerdbRegex.MatchString(d.Name()) {
backupDirPath = filepath.Dir(path)
return filepath.SkipDir
}
return nil
})
return backupDirPath, err
}
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)

View File

@@ -2,12 +2,17 @@ package chisel
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
)
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
if endpoint.Edge.AsyncMode {
return
}
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel := service.getTunnelDetails(endpoint.ID)
existingJobIndex := -1
for idx, existingJob := range tunnel.Jobs {
@@ -23,6 +28,8 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
tunnel.Jobs[existingJobIndex] = *edgeJob
}
cache.Del(endpoint.ID)
service.mu.Unlock()
}
@@ -30,8 +37,7 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
for _, tunnel := range service.tunnelDetailsMap {
// Filter in-place
for endpointID, tunnel := range service.tunnelDetailsMap {
n := 0
for _, edgeJob := range tunnel.Jobs {
if edgeJob.ID != edgeJobID {
@@ -41,7 +47,28 @@ func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
}
tunnel.Jobs = tunnel.Jobs[:n]
cache.Del(endpointID)
}
service.mu.Unlock()
}
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
n := 0
for _, edgeJob := range tunnel.Jobs {
if edgeJob.ID != edgeJobID {
tunnel.Jobs[n] = edgeJob
n++
}
}
tunnel.Jobs = tunnel.Jobs[:n]
cache.Del(endpointID)
service.mu.Unlock()
}

View File

@@ -206,15 +206,23 @@ func (service *Service) checkTunnels() {
service.mu.Lock()
for key, tunnel := range service.tunnelDetailsMap {
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
continue
}
if tunnel.Status == portainer.EdgeAgentManagementRequired && time.Since(tunnel.LastActivity) < requiredTimeout {
continue
}
if tunnel.Status == portainer.EdgeAgentActive && time.Since(tunnel.LastActivity) < activeTimeout {
continue
}
tunnels[key] = *tunnel
}
service.mu.Unlock()
for endpointID, tunnel := range tunnels {
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
continue
}
elapsed := time.Since(tunnel.LastActivity)
log.Debug().
Int("endpoint_id", int(endpointID)).
@@ -222,9 +230,7 @@ func (service *Service) checkTunnels() {
Float64("status_time_seconds", elapsed.Seconds()).
Msg("environment tunnel monitoring")
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed > requiredTimeout {
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
@@ -233,9 +239,7 @@ func (service *Service) checkTunnels() {
Msg("REQUIRED state timeout exceeded")
}
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
if tunnel.Status == portainer.EdgeAgentActive && elapsed > activeTimeout {
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).

View File

@@ -2,14 +2,17 @@ package chisel
import (
"encoding/base64"
"errors"
"fmt"
"math/rand"
"strings"
"time"
"github.com/dchest/uniuri"
"github.com/portainer/libcrypto"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/dchest/uniuri"
)
const (
@@ -49,6 +52,8 @@ func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *porta
service.tunnelDetailsMap[endpointID] = tunnel
cache.Del(endpointID)
return tunnel
}
@@ -62,6 +67,10 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portai
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
if endpoint.Edge.AsyncMode {
return portainer.TunnelDetails{}, errors.New("cannot open tunnel on async endpoint")
}
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentActive {
@@ -99,6 +108,8 @@ func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID)
tunnel.Credentials = ""
tunnel.LastActivity = time.Now()
service.mu.Unlock()
cache.Del(endpointID)
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
@@ -121,6 +132,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.ProxyManager.DeleteEndpointProxy(endpointID)
service.mu.Unlock()
cache.Del(endpointID)
}
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
@@ -129,6 +142,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
defer cache.Del(endpointID)
tunnel := service.getTunnelDetails(endpointID)
service.mu.Lock()

View File

@@ -36,7 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
@@ -62,6 +62,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("PRETTY", "JSON"),
}
kingpin.Parse()
@@ -71,6 +72,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
if err != nil {
panic(err)
}
*flags.Assets = filepath.Join(filepath.Dir(ex), *flags.Assets)
}
@@ -79,7 +81,6 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
// ValidateFlags validates the values of the flags.
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
err := validateEndpointURL(*flags.EndpointURL)
@@ -110,31 +111,38 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
}
func validateEndpointURL(endpointURL string) error {
if endpointURL != "" {
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
return errInvalidEndpointProtocol
}
if endpointURL == "" {
return nil
}
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
socketPath := strings.TrimPrefix(endpointURL, "unix://")
socketPath = strings.TrimPrefix(socketPath, "npipe://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketOrNamedPipeNotFound
}
return err
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
return errInvalidEndpointProtocol
}
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
socketPath := strings.TrimPrefix(endpointURL, "unix://")
socketPath = strings.TrimPrefix(socketPath, "npipe://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketOrNamedPipeNotFound
}
return err
}
}
return nil
}
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != "" {
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval
}
if snapshotInterval == "" {
return nil
}
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval
}
return nil
}

View File

@@ -2,23 +2,24 @@ package cli
import (
"bufio"
"log"
"fmt"
"os"
"strings"
)
// Confirm starts a rollback db cli application
func Confirm(message string) (bool, error) {
log.Printf("%s [y/N]", message)
fmt.Printf("%s [y/N]", message)
reader := bufio.NewReader(os.Stdin)
answer, err := reader.ReadString('\n')
if err != nil {
return false, err
}
answer = strings.Replace(answer, "\n", "", -1)
answer = strings.ReplaceAll(answer, "\n", "")
answer = strings.ToLower(answer)
return answer == "y" || answer == "yes", nil
}

View File

@@ -1,28 +0,0 @@
package main
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/rs/zerolog/log"
)
func importFromJson(fileService portainer.FileService, store *datastore.Store) {
// EXPERIMENTAL - if used with an incomplete json file, it will fail, as we don't have a way to default the model values
importFile := "/data/import.json"
if exists, _ := fileService.FileExists(importFile); exists {
if err := store.Import(importFile); err != nil {
log.Error().Str("filename", importFile).Err(err).Msg("import failed")
// TODO: should really rollback on failure, but then we have nothing.
} else {
log.Info().Str("filename", importFile).Msg("successfully imported the file to a new portainer database")
}
// TODO: this is bad - its to ensure that any defaults that were broken in import, or migrations get set back to what we want
// I also suspect that everything from "Init to Init" is potentially a migration
err := store.Init()
if err != nil {
log.Fatal().Err(err).Msg("failed initializing data store")
}
}
}

View File

@@ -1,7 +1,9 @@
package main
import (
"fmt"
stdlog "log"
"os"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -31,3 +33,23 @@ func setLoggingLevel(level string) {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
}
func setLoggingMode(mode string) {
switch mode {
case "PRETTY":
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
NoColor: true,
TimeFormat: "2006/01/02 03:04PM",
FormatMessage: formatMessage})
case "JSON":
log.Logger = log.Output(os.Stderr)
}
}
func formatMessage(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("%s |", i)
}

View File

@@ -3,14 +3,14 @@ package main
import (
"context"
"crypto/sha256"
"fmt"
"math/rand"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/portainer/libhelm"
libstack "github.com/portainer/docker-compose-wrapper"
"github.com/portainer/docker-compose-wrapper/compose"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/build"
@@ -19,6 +19,7 @@ import (
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/database"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/demo"
@@ -33,16 +34,21 @@ import (
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/internal/ssl"
"github.com/portainer/portainer/api/internal/upgrade"
"github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/kubernetes"
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/ldap"
"github.com/portainer/portainer/api/oauth"
"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/gofrs/uuid"
"github.com/rs/zerolog/log"
)
@@ -98,8 +104,6 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
log.Info().Msg("exiting rollback")
os.Exit(0)
return nil
}
// Init sets some defaults - it's basically a migration
@@ -109,24 +113,27 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
if isNew {
// from MigrateData
store.VersionService.StoreDBVersion(portainer.DBVersion)
instanceId, err := uuid.NewV4()
if err != nil {
log.Fatal().Err(err).Msg("failed generating instance id")
}
err := updateSettingsFromFlags(store, flags)
// from MigrateData
v := models.Version{
SchemaVersion: portainer.APIVersion,
Edition: int(portainer.PortainerCE),
InstanceID: instanceId.String(),
}
store.VersionService.UpdateVersion(&v)
err = updateSettingsFromFlags(store, flags)
if err != nil {
log.Fatal().Err(err).Msg("failed updating settings from flags")
}
} else {
storedVersion, err := store.VersionService.DBVersion()
err = store.MigrateData()
if err != nil {
log.Fatal().Err(err).Msg("failure during creation of new database")
}
if storedVersion != portainer.DBVersion {
err = store.MigrateData()
if err != nil {
log.Fatal().Err(err).Msg("failed migration")
}
log.Fatal().Err(err).Msg("failed migration")
}
}
@@ -139,22 +146,13 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
go func() {
<-shutdownCtx.Done()
defer connection.Close()
exportFilename := path.Join(*flags.Data, fmt.Sprintf("export-%d.json", time.Now().Unix()))
err := store.Export(exportFilename)
if err != nil {
log.Error().Str("filename", exportFilename).Err(err).Msg("failed to export")
} else {
log.Debug().Str("filename", exportFilename).Msg("exported")
}
}()
return store
}
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
func initComposeStackManager(composeDeployer libstack.Deployer, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
if err != nil {
log.Fatal().Err(err).Msg("failed creating compose manager")
}
@@ -239,8 +237,8 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService,
return docker.NewClientFactory(signatureService, reverseTunnelService)
}
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *kubecli.ClientFactory {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
}
func initSnapshotService(
@@ -316,51 +314,7 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
sslSettings.HTTPEnabled = true
}
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
if err != nil {
return err
}
return nil
}
// enableFeaturesFromFlags turns on or off feature flags
// e.g. portainer --feat open-amt --feat fdo=true ... (defaults to true)
// note, settings are persisted to the DB. To turn off `--feat open-amt=false`
func enableFeaturesFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
settings, err := dataStore.Settings().Settings()
if err != nil {
return err
}
if settings.FeatureFlagSettings == nil {
settings.FeatureFlagSettings = make(map[portainer.Feature]bool)
}
// loop through feature flags to check if they are supported
for _, feat := range *flags.FeatureFlags {
var correspondingFeature *portainer.Feature
for i, supportedFeat := range portainer.SupportedFeatureFlags {
if strings.EqualFold(feat.Name, string(supportedFeat)) {
correspondingFeature = &portainer.SupportedFeatureFlags[i]
}
}
if correspondingFeature == nil {
return fmt.Errorf("unknown feature flag '%s'", feat.Name)
}
featureState, err := strconv.ParseBool(feat.Value)
if err != nil {
return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name)
}
log.Info().Str("feature", string(*correspondingFeature)).Bool("state", featureState).Msg("")
settings.FeatureFlagSettings[*correspondingFeature] = featureState
}
return dataStore.Settings().UpdateSettings(settings)
return dataStore.SSLSettings().UpdateSettings(sslSettings)
}
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
@@ -553,6 +507,10 @@ func loadEncryptionSecretKey(keyfilename string) []byte {
func buildServer(flags *portainer.CLIFlags) portainer.Server {
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
if flags.FeatureFlags != nil {
featureflags.Parse(*flags.FeatureFlags, portainer.SupportedFeatureFlags)
}
fileService := initFileService(*flags.Data)
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
if encryptionKey == nil {
@@ -582,11 +540,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Err(err).Msg("failed initializing JWT service")
}
err = enableFeaturesFromFlags(dataStore, flags)
if err != nil {
log.Fatal().Err(err).Msg("failed enabling feature flag")
}
ldapService := initLDAPService()
oauthService := initOAuthService()
@@ -599,6 +552,8 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
digitalSignatureService := initDigitalSignatureService()
edgeStacksService := edgestacks.NewService(dataStore)
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
if err != nil {
log.Fatal().Err(err).Msg("")
@@ -617,7 +572,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil {
@@ -638,7 +593,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
dockerConfigPath := fileService.GetDockerConfigPath()
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing compose deployer")
}
composeStackManager := initComposeStackManager(composeDeployer, reverseTunnelService, proxyManager)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
@@ -716,14 +676,32 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
scheduler := scheduler.NewScheduler(shutdownCtx)
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
stacks.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
stackDeployer := deployments.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
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{
AuthorizationService: authorizationService,
ReverseTunnelService: reverseTunnelService,
@@ -733,6 +711,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
HTTPEnabled: sslDBSettings.HTTPEnabled,
AssetsPath: *flags.Assets,
DataStore: dataStore,
EdgeStacksService: edgeStacksService,
SwarmStackManager: swarmStackManager,
ComposeStackManager: composeStackManager,
KubernetesDeployer: kubernetesDeployer,
@@ -758,15 +737,20 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
DemoService: demoService,
UpgradeService: upgradeService,
}
}
func main() {
rand.Seed(time.Now().UnixNano())
configureLogger()
setLoggingMode("PRETTY")
flags := initCLI()
setLoggingLevel(*flags.LogLevel)
setLoggingMode(*flags.LogMode)
for {
server := buildServer(flags)

View File

@@ -1,111 +0,0 @@
package main
import (
"fmt"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
"gopkg.in/alecthomas/kingpin.v2"
)
type mockKingpinSetting string
func (m mockKingpinSetting) SetValue(value kingpin.Value) {
value.Set(string(m))
}
func Test_enableFeaturesFromFlags(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(t, true, true)
defer teardown()
tests := []struct {
featureFlag string
isSupported bool
}{
{"test", false},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
mockKingpinSetting := mockKingpinSetting(test.featureFlag)
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
err := enableFeaturesFromFlags(store, flags)
if test.isSupported {
is.NoError(err)
} else {
is.Error(err)
}
})
}
t.Run("passes for all supported feature flags", func(t *testing.T) {
for _, flag := range portainer.SupportedFeatureFlags {
mockKingpinSetting := mockKingpinSetting(flag)
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
err := enableFeaturesFromFlags(store, flags)
is.NoError(err)
}
})
}
const FeatTest portainer.Feature = "optional-test"
func optionalFunc(dataStore dataservices.DataStore) string {
// TODO: this is a code smell - finding out if a feature flag is enabled should not require having access to the store, and asking for a settings obj.
// ideally, the `if` should look more like:
// if featureflags.FlagEnabled(FeatTest) {}
settings, err := dataStore.Settings().Settings()
if err != nil {
return err.Error()
}
if settings.FeatureFlagSettings[FeatTest] {
return "enabled"
}
return "disabled"
}
func Test_optionalFeature(t *testing.T) {
portainer.SupportedFeatureFlags = append(portainer.SupportedFeatureFlags, FeatTest)
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(t, true, true)
defer teardown()
// Enable the test feature
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
mockKingpinSetting := mockKingpinSetting(FeatTest)
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
err := enableFeaturesFromFlags(store, flags)
is.NoError(err)
is.Equal("enabled", optionalFunc(store))
})
// Same store, so the feature flag should still be enabled
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
is.Equal("enabled", optionalFunc(store))
})
// disable the test feature
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
mockKingpinSetting := mockKingpinSetting(FeatTest + "=false")
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
err := enableFeaturesFromFlags(store, flags)
is.NoError(err)
is.Equal("disabled", optionalFunc(store))
})
// Same store, so feature flag should still be disabled
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
is.Equal("disabled", optionalFunc(store))
})
}

View File

@@ -4,10 +4,35 @@ import (
"io"
)
type ReadTransaction interface {
GetObject(bucketName string, key []byte, object interface{}) error
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error
}
type Transaction interface {
ReadTransaction
SetServiceName(bucketName string) error
UpdateObject(bucketName string, key []byte, object interface{}) error
DeleteObject(bucketName string, key []byte) error
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
CreateObjectWithId(bucketName string, id int, obj interface{}) error
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error
GetNextIdentifier(bucketName string) int
}
type Connection interface {
Transaction
Open() error
Close() error
UpdateTx(fn func(Transaction) error) error
ViewTx(fn func(Transaction) error) error
// write the db contents to filename as json (the schema needs defining)
ExportRaw(filename string) error
@@ -21,19 +46,9 @@ type Connection interface {
NeedsEncryptionMigration() (bool, error)
SetEncrypted(encrypted bool)
SetServiceName(bucketName string) error
GetObject(bucketName string, key []byte, object interface{}) error
UpdateObject(bucketName string, key []byte, object interface{}) error
DeleteObject(bucketName string, key []byte) error
DeleteAllObjects(bucketName string, matching func(o interface{}) (id int, ok bool)) error
GetNextIdentifier(bucketName string) int
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
CreateObjectWithId(bucketName string, id int, obj interface{}) error
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
ConvertToKey(v int) []byte
BackupMetadata() (map[string]interface{}, error)
RestoreMetadata(s map[string]interface{}) error
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
ConvertToKey(v int) []byte
}

View File

@@ -13,7 +13,7 @@ import (
// Person with better knowledge is welcomed to improve it.
// sourced from https://golang.org/src/crypto/cipher/example_test.go
var emptySalt []byte = make([]byte, 0, 0)
var emptySalt []byte = make([]byte, 0)
// AesEncrypt reads from input, encrypts with AES-256 and writes to the output.
// passphrase is used to generate an encryption key.

View File

@@ -2,7 +2,6 @@ package crypto
import (
"io"
"io/ioutil"
"os"
"path/filepath"
"testing"
@@ -20,7 +19,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
@@ -30,7 +29,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
encryptedContent, err := os.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
@@ -45,7 +44,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
@@ -59,7 +58,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
@@ -69,7 +68,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
err := AesEncrypt(originFile, encryptedFileWriter, []byte(""))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
encryptedContent, err := os.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
@@ -84,7 +83,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
@@ -98,7 +97,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
)
content := []byte("content")
ioutil.WriteFile(originFilePath, content, 0600)
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
@@ -108,7 +107,7 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
err := AesEncrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := ioutil.ReadFile(encryptedFilePath)
encryptedContent, err := os.ReadFile(encryptedFilePath)
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
@@ -123,6 +122,6 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, _ := ioutil.ReadFile(decryptedFilePath)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.NotEqual(t, content, decryptedContent, "Original and decrypted content should NOT match")
}

View File

@@ -7,7 +7,6 @@ import (
"crypto/x509"
"encoding/base64"
"encoding/hex"
"math/big"
"github.com/portainer/libcrypto"
)
@@ -115,9 +114,6 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
hash := libcrypto.HashFromBytes([]byte(message))
r := big.NewInt(0)
s := big.NewInt(0)
r, s, err := ecdsa.Sign(rand.Reader, service.privateKey, hash)
if err != nil {
return "", err

View File

@@ -3,11 +3,11 @@ package crypto
import (
"crypto/tls"
"crypto/x509"
"io/ioutil"
"os"
)
// CreateServerTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
func CreateServerTLSConfiguration() *tls.Config {
// CreateTLSConfiguration creates a basic tls.Config with recommended TLS settings
func CreateTLSConfiguration() *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
@@ -27,7 +27,7 @@ func CreateServerTLSConfiguration() *tls.Config {
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
// loaded from memory.
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
config := &tls.Config{}
config := CreateTLSConfiguration()
config.InsecureSkipVerify = skipServerVerification
if !skipClientVerification {
@@ -50,7 +50,7 @@ func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerific
// CreateTLSConfigurationFromDisk initializes a tls.Config using a CA certificate, a certificate and a key
// loaded from disk.
func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipServerVerification bool) (*tls.Config, error) {
config := &tls.Config{}
config := CreateTLSConfiguration()
config.InsecureSkipVerify = skipServerVerification
if certPath != "" && keyPath != "" {
@@ -63,7 +63,7 @@ func CreateTLSConfigurationFromDisk(caCertPath, certPath, keyPath string, skipSe
}
if !skipServerVerification && caCertPath != "" {
caCert, err := ioutil.ReadFile(caCertPath)
caCert, err := os.ReadFile(caCertPath)
if err != nil {
return nil, err
}

View File

@@ -1,16 +1,15 @@
package boltdb
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"time"
portainer "github.com/portainer/portainer/api"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
@@ -133,9 +132,11 @@ func (connection *DbConnection) Open() error {
if err != nil {
return err
}
db.MaxBatchSize = connection.MaxBatchSize
db.MaxBatchDelay = connection.MaxBatchDelay
connection.DB = db
return nil
}
@@ -145,9 +146,30 @@ func (connection *DbConnection) Close() error {
if connection.DB != nil {
return connection.DB.Close()
}
return nil
}
func (connection *DbConnection) txFn(fn func(portainer.Transaction) error) func(*bolt.Tx) error {
return func(tx *bolt.Tx) error {
return fn(&DbTransaction{conn: connection, tx: tx})
}
}
// UpdateTx executes the given function inside a read-write transaction
func (connection *DbConnection) UpdateTx(fn func(portainer.Transaction) error) error {
if connection.MaxBatchDelay > 0 && connection.MaxBatchSize > 1 {
return connection.Batch(connection.txFn(fn))
}
return connection.Update(connection.txFn(fn))
}
// ViewTx executes the given function inside a read-only transaction
func (connection *DbConnection) ViewTx(fn func(portainer.Transaction) error) error {
return connection.View(connection.txFn(fn))
}
// BackupTo backs up db to a provided writer.
// It does hot backup and doesn't block other database reads and writes
func (connection *DbConnection) BackupTo(w io.Writer) error {
@@ -167,7 +189,7 @@ func (connection *DbConnection) ExportRaw(filename string) error {
if err != nil {
return err
}
return ioutil.WriteFile(filename, b, 0600)
return os.WriteFile(filename, b, 0600)
}
// ConvertToKey returns an 8-byte big endian representation of v.
@@ -179,36 +201,18 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
return b
}
// CreateBucket is a generic function used to create a bucket inside a database database.
// CreateBucket is a generic function used to create a bucket inside a database.
func (connection *DbConnection) SetServiceName(bucketName string) error {
return connection.Batch(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
return err
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.SetServiceName(bucketName)
})
}
// GetObject is a generic function used to retrieve an unmarshalled object from a database database.
// GetObject is a generic function used to retrieve an unmarshalled object from a database.
func (connection *DbConnection) GetObject(bucketName string, key []byte, object interface{}) error {
var data []byte
err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
if value == nil {
return dserrors.ErrObjectNotFound
}
data = make([]byte, len(value))
copy(data, value)
return nil
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(bucketName, key, object)
})
if err != nil {
return err
}
return connection.UnmarshalObjectWithJsoniter(data, object)
}
func (connection *DbConnection) getEncryptionKey() []byte {
@@ -219,50 +223,51 @@ func (connection *DbConnection) getEncryptionKey() []byte {
return connection.EncryptionKey
}
// UpdateObject is a generic function used to update an object inside a database database.
// UpdateObject is a generic function used to update an object inside a database.
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
data, err := connection.MarshalObject(object)
if err != nil {
return err
}
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.UpdateObject(bucketName, key, object)
})
}
// UpdateObjectFunc is a generic function used to update an object safely without race conditions.
func (connection *DbConnection) UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data := bucket.Get(key)
if data == nil {
return dserrors.ErrObjectNotFound
}
err := connection.UnmarshalObjectWithJsoniter(data, object)
if err != nil {
return err
}
updateFn()
data, err = connection.MarshalObject(object)
if err != nil {
return err
}
return bucket.Put(key, data)
})
}
// DeleteObject is a generic function used to delete an object inside a database database.
// DeleteObject is a generic function used to delete an object inside a database.
func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.DeleteObject(bucketName, key)
})
}
// DeleteAllObjects delete all objects where matching() returns (id, ok).
// TODO: think about how to return the error inside (maybe change ok to type err, and use "notfound"?
func (connection *DbConnection) DeleteAllObjects(bucketName string, matching func(o interface{}) (id int, ok bool)) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var obj interface{}
err := connection.UnmarshalObject(v, &obj)
if err != nil {
return err
}
if id, ok := matching(obj); ok {
err := bucket.Delete(connection.ConvertToKey(id))
if err != nil {
return err
}
}
}
return nil
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.DeleteAllObjects(bucketName, obj, matching)
})
}
@@ -270,13 +275,8 @@ func (connection *DbConnection) DeleteAllObjects(bucketName string, matching fun
func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
var identifier int
connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {
return err
}
identifier = int(id)
_ = connection.UpdateTx(func(tx portainer.Transaction) error {
identifier = tx.GetNextIdentifier(bucketName)
return nil
})
@@ -285,108 +285,41 @@ func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
// CreateObject creates a new object in the bucket, using the next bucket sequence id
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
seqId, _ := bucket.NextSequence()
id, obj := fn(seqId)
data, err := connection.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(connection.ConvertToKey(int(id)), data)
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.CreateObject(bucketName, fn)
})
}
// CreateObjectWithId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := connection.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(connection.ConvertToKey(id), data)
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.CreateObjectWithId(bucketName, id, obj)
})
}
// CreateObjectWithStringId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := connection.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(id, data)
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.CreateObjectWithStringId(bucketName, id, obj)
})
}
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := connection.UnmarshalObject(v, obj)
if err != nil {
return err
}
obj, err = append(obj)
if err != nil {
return err
}
}
return nil
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAll(bucketName, obj, append)
})
return err
}
// TODO: decide which Unmarshal to use, and use one...
func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := connection.UnmarshalObjectWithJsoniter(v, obj)
if err != nil {
return err
}
obj, err = append(obj)
if err != nil {
return err
}
}
return nil
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAllWithJsoniter(bucketName, obj, append)
})
return err
}
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
return connection.View(func(tx *bolt.Tx) error {
cursor := tx.Bucket([]byte(bucketName)).Cursor()
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
err := connection.UnmarshalObjectWithJsoniter(v, obj)
if err != nil {
return err
}
obj, err = append(obj)
if err != nil {
return err
}
}
return nil
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, append)
})
}
@@ -397,7 +330,6 @@ func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error)
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
bucket = tx.Bucket([]byte(bucketName))
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil

View File

@@ -14,7 +14,6 @@ func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
bucket = tx.Bucket([]byte(bucketName))
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil

View File

@@ -129,7 +129,7 @@ func Test_UnMarshalObjectUnencrypted(t *testing.T) {
var object string
err := conn.UnmarshalObject(test.object, &object)
is.NoError(err)
is.Equal(test.expected, string(object))
is.Equal(test.expected, object)
})
}
}

172
api/database/boltdb/tx.go Normal file
View File

@@ -0,0 +1,172 @@
package boltdb
import (
"bytes"
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 dserrors.ErrObjectNotFound
}
data := make([]byte, len(value))
copy(data, value)
return tx.conn.UnmarshalObjectWithJsoniter(data, object)
}
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object interface{}) error {
data, err := tx.conn.MarshalObject(object)
if err != nil {
return err
}
bucket := tx.tx.Bucket([]byte(bucketName))
return bucket.Put(key, data)
}
func (tx *DbTransaction) DeleteObject(bucketName string, key []byte) error {
bucket := tx.tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
}
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := tx.conn.UnmarshalObject(v, &obj)
if err != nil {
return err
}
if id, ok := matching(obj); ok {
err := bucket.Delete(tx.conn.ConvertToKey(id))
if err != nil {
return err
}
}
}
return nil
}
func (tx *DbTransaction) GetNextIdentifier(bucketName string) int {
bucket := tx.tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifer")
return 0
}
return int(id)
}
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
bucket := tx.tx.Bucket([]byte(bucketName))
seqId, _ := bucket.NextSequence()
id, obj := fn(seqId)
data, err := tx.conn.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(tx.conn.ConvertToKey(id), data)
}
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
bucket := tx.tx.Bucket([]byte(bucketName))
data, err := tx.conn.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(tx.conn.ConvertToKey(id), data)
}
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
bucket := tx.tx.Bucket([]byte(bucketName))
data, err := tx.conn.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(id, data)
}
func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := tx.conn.UnmarshalObject(v, obj)
if err != nil {
return err
}
obj, err = append(obj)
if err != nil {
return err
}
}
return nil
}
func (tx *DbTransaction) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
if err != nil {
return err
}
obj, err = append(obj)
if err != nil {
return err
}
}
return nil
}
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
cursor := tx.tx.Bucket([]byte(bucketName)).Cursor()
for k, v := cursor.Seek(keyPrefix); k != nil && bytes.HasPrefix(k, keyPrefix); k, v = cursor.Next() {
err := tx.conn.UnmarshalObjectWithJsoniter(v, obj)
if err != nil {
return err
}
obj, err = append(obj)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,126 @@
package boltdb
import (
"errors"
"testing"
portainer "github.com/portainer/portainer/api"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
)
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 err != dserrors.ErrObjectNotFound {
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")
}
}

View File

@@ -9,8 +9,7 @@ import (
// NewDatabase should use config options to return a connection to the requested database
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
switch storeType {
case "boltdb":
if storeType == "boltdb" {
return &boltdb.DbConnection{
Path: storePath,
EncryptionKey: encryptionKey,

View File

@@ -0,0 +1,8 @@
package models
type Version struct {
SchemaVersion string
MigratorCount int
Edition int
InstanceID string
}

View File

@@ -1,17 +1,11 @@
package edgegroup
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgegroups"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "edgegroups"
// Service represents a service for managing Edge group data.
type Service struct {
@@ -34,59 +28,65 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
// EdgeGroups return an array containing all the Edge groups.
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EdgeGroups return a slice containing all the Edge groups.
func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 0)
var groups []portainer.EdgeGroup
var err error
err := service.connection.GetAllWithJsoniter(
BucketName,
&portainer.EdgeGroup{},
func(obj interface{}) (interface{}, error) {
group, ok := obj.(*portainer.EdgeGroup)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
}
groups = append(groups, *group)
return &portainer.EdgeGroup{}, nil
})
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
groups, err = service.Tx(tx).EdgeGroups()
return err
})
return groups, err
}
// EdgeGroup returns an Edge group by ID.
func (service *Service) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
var group portainer.EdgeGroup
identifier := service.connection.ConvertToKey(int(ID))
var group *portainer.EdgeGroup
var err error
err := service.connection.GetObject(BucketName, identifier, &group)
if err != nil {
return nil, err
}
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
group, err = service.Tx(tx).EdgeGroup(ID)
return err
})
return &group, nil
return group, err
}
// UpdateEdgeGroup updates an Edge group.
// UpdateEdgeGroup updates an edge group.
func (service *Service) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, group)
}
// Deprecated: UpdateEdgeGroupFunc updates an edge group inside a transaction avoiding data races.
func (service *Service) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
id := service.connection.ConvertToKey(int(ID))
edgeGroup := &portainer.EdgeGroup{}
return service.connection.UpdateObjectFunc(BucketName, id, edgeGroup, func() {
updateFunc(edgeGroup)
})
}
// DeleteEdgeGroup deletes an Edge group.
func (service *Service) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).DeleteEdgeGroup(ID)
})
}
// CreateEdgeGroup assign an ID to a new Edge group and saves it.
func (service *Service) Create(group *portainer.EdgeGroup) error {
return service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
group.ID = portainer.EdgeGroupID(id)
return int(group.ID), group
},
)
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).Create(group)
})
}

View File

@@ -0,0 +1,80 @@
package edgegroup
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// EdgeGroups return a slice containing all the Edge groups.
func (service ServiceTx) EdgeGroups() ([]portainer.EdgeGroup, error) {
var groups = make([]portainer.EdgeGroup, 0)
err := service.tx.GetAllWithJsoniter(
BucketName,
&portainer.EdgeGroup{},
func(obj interface{}) (interface{}, error) {
group, ok := obj.(*portainer.EdgeGroup)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
}
groups = append(groups, *group)
return &portainer.EdgeGroup{}, nil
})
return groups, err
}
// EdgeGroup returns an Edge group by ID.
func (service ServiceTx) EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
var group portainer.EdgeGroup
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &group)
if err != nil {
return nil, err
}
return &group, nil
}
// UpdateEdgeGroup updates an edge group.
func (service ServiceTx) UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, group)
}
// UpdateEdgeGroupFunc is a no-op inside a transaction.
func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(edgeGroup *portainer.EdgeGroup)) error {
return errors.New("cannot be called inside a transaction")
}
// DeleteEdgeGroup deletes an Edge group.
func (service ServiceTx) DeleteEdgeGroup(ID portainer.EdgeGroupID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
return service.tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
group.ID = portainer.EdgeGroupID(id)
return int(group.ID), group
},
)
}

View File

@@ -8,10 +8,8 @@ import (
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edgejobs"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "edgejobs"
// Service represents a service for managing edge jobs data.
type Service struct {
@@ -34,6 +32,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EdgeJobs returns a list of Edge jobs
func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
@@ -80,12 +85,22 @@ func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJo
)
}
// UpdateEdgeJob updates an Edge job by ID
// Deprecated: use UpdateEdgeJobFunc instead
func (service *Service) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, edgeJob)
}
// UpdateEdgeJobFunc updates an edge job inside a transaction avoiding data races.
func (service *Service) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
id := service.connection.ConvertToKey(int(ID))
edgeJob := &portainer.EdgeJob{}
return service.connection.UpdateObjectFunc(BucketName, id, edgeJob, func() {
updateFunc(edgeJob)
})
}
// DeleteEdgeJob deletes an Edge job
func (service *Service) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := service.connection.ConvertToKey(int(ID))

View File

@@ -0,0 +1,84 @@
package edgejob
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// EdgeJobs returns a list of Edge jobs
func (service ServiceTx) EdgeJobs() ([]portainer.EdgeJob, error) {
var edgeJobs = make([]portainer.EdgeJob, 0)
err := service.tx.GetAll(
BucketName,
&portainer.EdgeJob{},
func(obj interface{}) (interface{}, error) {
job, ok := obj.(*portainer.EdgeJob)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeJob object")
return nil, fmt.Errorf("failed to convert to EdgeJob object: %s", obj)
}
edgeJobs = append(edgeJobs, *job)
return &portainer.EdgeJob{}, nil
})
return edgeJobs, err
}
// EdgeJob returns an Edge job by ID
func (service ServiceTx) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error) {
var edgeJob portainer.EdgeJob
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &edgeJob)
if err != nil {
return nil, err
}
return &edgeJob, nil
}
// Create creates a new EdgeJob
func (service ServiceTx) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
edgeJob.ID = ID
return service.tx.CreateObjectWithId(BucketName, int(edgeJob.ID), edgeJob)
}
// UpdateEdgeJob updates an edge job
func (service ServiceTx) UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, edgeJob)
}
// UpdateEdgeJobFunc is a no-op inside a transaction.
func (service ServiceTx) UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error {
return errors.New("cannot be called inside a transaction")
}
// DeleteEdgeJob deletes an Edge job
func (service ServiceTx) DeleteEdgeJob(ID portainer.EdgeJobID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service ServiceTx) GetNextIdentifier() int {
return service.tx.GetNextIdentifier(BucketName)
}

View File

@@ -2,20 +2,22 @@ package edgestack
import (
"fmt"
"sync"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edge_stack"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "edge_stack"
// Service represents a service for managing Edge stack data.
type Service struct {
connection portainer.Connection
connection portainer.Connection
idxVersion map[portainer.EdgeStackID]int
mu sync.RWMutex
cacheInvalidationFn func(portainer.EdgeStackID)
}
func (service *Service) BucketName() string {
@@ -23,15 +25,39 @@ func (service *Service) BucketName() string {
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
func NewService(connection portainer.Connection, cacheInvalidationFn func(portainer.EdgeStackID)) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
s := &Service{
connection: connection,
idxVersion: make(map[portainer.EdgeStackID]int),
cacheInvalidationFn: cacheInvalidationFn,
}
if s.cacheInvalidationFn == nil {
s.cacheInvalidationFn = func(portainer.EdgeStackID) {}
}
es, err := s.EdgeStacks()
if err != nil {
return nil, err
}
for _, e := range es {
s.idxVersion[e.ID] = e.Version
}
return s, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EdgeStacks returns an array containing all edge stacks
@@ -42,7 +68,6 @@ func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
BucketName,
&portainer.EdgeStack{},
func(obj interface{}) (interface{}, error) {
//var tag portainer.Tag
stack, ok := obj.(*portainer.EdgeStack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
@@ -70,28 +95,92 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
return &stack, nil
}
// EdgeStackVersion returns the version of the given edge stack ID directly from an in-memory index
func (service *Service) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool) {
service.mu.RLock()
v, ok := service.idxVersion[ID]
service.mu.RUnlock()
return v, ok
}
// CreateEdgeStack saves an Edge stack object to db.
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
edgeStack.ID = id
return service.connection.CreateObjectWithId(
err := service.connection.CreateObjectWithId(
BucketName,
int(edgeStack.ID),
edgeStack,
)
if err != nil {
return err
}
service.mu.Lock()
service.idxVersion[id] = edgeStack.Version
service.cacheInvalidationFn(id)
service.mu.Unlock()
return nil
}
// UpdateEdgeStack updates an Edge stack.
// Deprecated: Use UpdateEdgeStackFunc instead.
func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
service.mu.Lock()
defer service.mu.Unlock()
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, edgeStack)
err := service.connection.UpdateObject(BucketName, identifier, edgeStack)
if err != nil {
return err
}
service.idxVersion[ID] = edgeStack.Version
service.cacheInvalidationFn(ID)
return nil
}
// UpdateEdgeStackFunc updates an Edge stack inside a transaction avoiding data races.
func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
id := service.connection.ConvertToKey(int(ID))
edgeStack := &portainer.EdgeStack{}
service.mu.Lock()
defer service.mu.Unlock()
return service.connection.UpdateObjectFunc(BucketName, id, edgeStack, func() {
updateFunc(edgeStack)
service.idxVersion[ID] = edgeStack.Version
service.cacheInvalidationFn(ID)
})
}
// UpdateEdgeStackFuncTx is a helper function used to call UpdateEdgeStackFunc inside a transaction.
func (service *Service) UpdateEdgeStackFuncTx(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error {
return service.Tx(tx).UpdateEdgeStackFunc(ID, updateFunc)
}
// DeleteEdgeStack deletes an Edge stack.
func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
service.mu.Lock()
defer service.mu.Unlock()
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
err := service.connection.DeleteObject(BucketName, identifier)
if err != nil {
return err
}
delete(service.idxVersion, ID)
service.cacheInvalidationFn(ID)
return nil
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).

View 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)
}

View File

@@ -1,185 +0,0 @@
package edgeupdateschedule
import (
"fmt"
"sync"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/edgetypes"
"github.com/sirupsen/logrus"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "edge_update_schedule"
)
// Service represents a service for managing Edge Update Schedule data.
type Service struct {
connection portainer.Connection
mu sync.Mutex
idxActiveSchedules map[portainer.EndpointID]*edgetypes.EndpointUpdateScheduleRelation
}
func (service *Service) BucketName() string {
return BucketName
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
service := &Service{
connection: connection,
}
service.idxActiveSchedules = map[portainer.EndpointID]*edgetypes.EndpointUpdateScheduleRelation{}
schedules, err := service.List()
if err != nil {
return nil, errors.WithMessage(err, "Unable to list schedules")
}
for _, schedule := range schedules {
service.setRelation(&schedule)
}
return service, nil
}
func (service *Service) ActiveSchedule(environmentID portainer.EndpointID) *edgetypes.EndpointUpdateScheduleRelation {
service.mu.Lock()
defer service.mu.Unlock()
return service.idxActiveSchedules[environmentID]
}
func (service *Service) ActiveSchedules(environmentsIDs []portainer.EndpointID) []edgetypes.EndpointUpdateScheduleRelation {
service.mu.Lock()
defer service.mu.Unlock()
schedules := []edgetypes.EndpointUpdateScheduleRelation{}
for _, environmentID := range environmentsIDs {
if s, ok := service.idxActiveSchedules[environmentID]; ok {
schedules = append(schedules, *s)
}
}
return schedules
}
// List return an array containing all the items in the bucket.
func (service *Service) List() ([]edgetypes.UpdateSchedule, error) {
var list = make([]edgetypes.UpdateSchedule, 0)
err := service.connection.GetAll(
BucketName,
&edgetypes.UpdateSchedule{},
func(obj interface{}) (interface{}, error) {
item, ok := obj.(*edgetypes.UpdateSchedule)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to EdgeUpdateSchedule object")
return nil, fmt.Errorf("Failed to convert to EdgeUpdateSchedule object: %s", obj)
}
list = append(list, *item)
return &edgetypes.UpdateSchedule{}, nil
})
return list, err
}
// Item returns a item by ID.
func (service *Service) Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error) {
var item edgetypes.UpdateSchedule
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &item)
if err != nil {
return nil, err
}
return &item, nil
}
// Create assign an ID to a new object and saves it.
func (service *Service) Create(item *edgetypes.UpdateSchedule) error {
err := service.connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
item.ID = edgetypes.UpdateScheduleID(id)
return int(item.ID), item
},
)
if err != nil {
return err
}
return service.setRelation(item)
}
// Update updates an item.
func (service *Service) Update(id edgetypes.UpdateScheduleID, item *edgetypes.UpdateSchedule) error {
identifier := service.connection.ConvertToKey(int(id))
err := service.connection.UpdateObject(BucketName, identifier, item)
if err != nil {
return err
}
service.cleanRelation(id)
return service.setRelation(item)
}
// Delete deletes an item.
func (service *Service) Delete(id edgetypes.UpdateScheduleID) error {
service.cleanRelation(id)
identifier := service.connection.ConvertToKey(int(id))
return service.connection.DeleteObject(BucketName, identifier)
}
func (service *Service) cleanRelation(id edgetypes.UpdateScheduleID) {
service.mu.Lock()
defer service.mu.Unlock()
for _, schedule := range service.idxActiveSchedules {
if schedule != nil && schedule.ScheduleID == id {
delete(service.idxActiveSchedules, schedule.EnvironmentID)
}
}
}
func (service *Service) setRelation(schedule *edgetypes.UpdateSchedule) error {
service.mu.Lock()
defer service.mu.Unlock()
for environmentID, environmentStatus := range schedule.Status {
if environmentStatus.Status != edgetypes.UpdateScheduleStatusPending {
continue
}
// this should never happen
if service.idxActiveSchedules[environmentID] != nil && service.idxActiveSchedules[environmentID].ScheduleID != schedule.ID {
return errors.New("Multiple schedules are pending for the same environment")
}
service.idxActiveSchedules[environmentID] = &edgetypes.EndpointUpdateScheduleRelation{
EnvironmentID: environmentID,
ScheduleID: schedule.ID,
TargetVersion: environmentStatus.TargetVersion,
Status: environmentStatus.Status,
Error: environmentStatus.Error,
Type: schedule.Type,
}
}
return nil
}

View File

@@ -1,21 +1,21 @@
package endpoint
import (
"fmt"
"sync"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoints"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "endpoints"
// Service represents a service for managing environment(endpoint) data.
type Service struct {
connection portainer.Connection
mu sync.RWMutex
idxEdgeID map[string]portainer.EndpointID
heartbeats sync.Map
}
func (service *Service) BucketName() string {
@@ -29,64 +29,130 @@ func NewService(connection portainer.Connection) (*Service, error) {
return nil, err
}
return &Service{
s := &Service{
connection: connection,
}, nil
}
idxEdgeID: make(map[string]portainer.EndpointID),
}
// Endpoint returns an environment(endpoint) by ID.
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint portainer.Endpoint
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &endpoint)
es, err := s.endpoints()
if err != nil {
return nil, err
}
return &endpoint, nil
for _, e := range es {
if len(e.EdgeID) > 0 {
s.idxEdgeID[e.EdgeID] = e.ID
}
s.heartbeats.Store(e.ID, e.LastCheckInDate)
}
return s, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// Endpoint returns an environment(endpoint) by ID.
func (service *Service) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint *portainer.Endpoint
var err error
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
endpoint, err = service.Tx(tx).Endpoint(ID)
return err
})
if err != nil {
return nil, err
}
endpoint.LastCheckInDate, _ = service.Heartbeat(ID)
return endpoint, nil
}
// UpdateEndpoint updates an environment(endpoint).
func (service *Service) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, endpoint)
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).UpdateEndpoint(ID, endpoint)
})
}
// DeleteEndpoint deletes an environment(endpoint).
func (service *Service) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).DeleteEndpoint(ID)
})
}
// Endpoints return an array containing all the environments(endpoints).
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
func (service *Service) endpoints() ([]portainer.Endpoint, error) {
var endpoints []portainer.Endpoint
var err error
err := service.connection.GetAllWithJsoniter(
BucketName,
&portainer.Endpoint{},
func(obj interface{}) (interface{}, error) {
endpoint, ok := obj.(*portainer.Endpoint)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Endpoint object")
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
}
endpoints = append(endpoints, *endpoint)
return &portainer.Endpoint{}, nil
})
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
endpoints, err = service.Tx(tx).Endpoints()
return err
})
return endpoints, err
}
// Endpoints return an array containing all the environments(endpoints).
func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
endpoints, err := service.endpoints()
if err != nil {
return nil, err
}
for i, e := range endpoints {
t, _ := service.Heartbeat(e.ID)
endpoints[i].LastCheckInDate = t
}
return endpoints, nil
}
// EndpointIDByEdgeID returns the EndpointID from the given EdgeID using an in-memory index
func (service *Service) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
service.mu.RLock()
endpointID, ok := service.idxEdgeID[edgeID]
service.mu.RUnlock()
return endpointID, ok
}
func (service *Service) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
if t, ok := service.heartbeats.Load(endpointID); ok {
return t.(int64), true
}
return 0, false
}
func (service *Service) UpdateHeartbeat(endpointID portainer.EndpointID) {
service.heartbeats.Store(endpointID, time.Now().Unix())
}
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
func (service *Service) Create(endpoint *portainer.Endpoint) error {
return service.connection.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
return service.connection.UpdateTx(func(tx portainer.Transaction) error {
return service.Tx(tx).Create(endpoint)
})
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
return service.connection.GetNextIdentifier(BucketName)
var identifier int
service.connection.UpdateTx(func(tx portainer.Transaction) error {
identifier = service.Tx(tx).GetNextIdentifier()
return nil
})
return identifier
}

View File

@@ -0,0 +1,137 @@
package endpoint
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// Endpoint returns an environment(endpoint) by ID.
func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) {
var endpoint portainer.Endpoint
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, nil
}
// UpdateEndpoint updates an environment(endpoint).
func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.UpdateObject(BucketName, identifier, endpoint)
if err != nil {
return err
}
service.service.mu.Lock()
if len(endpoint.EdgeID) > 0 {
service.service.idxEdgeID[endpoint.EdgeID] = ID
}
service.service.heartbeats.Store(ID, endpoint.LastCheckInDate)
service.service.mu.Unlock()
cache.Del(endpoint.ID)
return nil
}
// DeleteEndpoint deletes an environment(endpoint).
func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.DeleteObject(BucketName, identifier)
if err != nil {
return err
}
service.service.mu.Lock()
for edgeID, endpointID := range service.service.idxEdgeID {
if endpointID == ID {
delete(service.service.idxEdgeID, edgeID)
break
}
}
service.service.heartbeats.Delete(ID)
service.service.mu.Unlock()
cache.Del(ID)
return nil
}
// Endpoints return an array containing all the environments(endpoints).
func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
err := service.tx.GetAllWithJsoniter(
BucketName,
&portainer.Endpoint{},
func(obj interface{}) (interface{}, error) {
endpoint, ok := obj.(*portainer.Endpoint)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Endpoint object")
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
}
endpoints = append(endpoints, *endpoint)
return &portainer.Endpoint{}, nil
})
return endpoints, err
}
func (service ServiceTx) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
log.Error().Str("func", "EndpointIDByEdgeID").Msg("cannot be called inside a transaction")
return 0, false
}
func (service ServiceTx) Heartbeat(endpointID portainer.EndpointID) (int64, bool) {
log.Error().Str("func", "Heartbeat").Msg("cannot be called inside a transaction")
return 0, false
}
func (service ServiceTx) UpdateHeartbeat(endpointID portainer.EndpointID) {
log.Error().Str("func", "UpdateHeartbeat").Msg("cannot be called inside a transaction")
}
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
if err != nil {
return err
}
service.service.mu.Lock()
if len(endpoint.EdgeID) > 0 {
service.service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
}
service.service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
service.service.mu.Unlock()
return nil
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service ServiceTx) GetNextIdentifier() int {
return service.tx.GetNextIdentifier(BucketName)
}

View File

@@ -34,6 +34,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EndpointGroup returns an environment(endpoint) group by ID.
func (service *Service) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var endpointGroup portainer.EndpointGroup

View File

@@ -0,0 +1,76 @@
package endpointgroup
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// EndpointGroup returns an environment(endpoint) group by ID.
func (service ServiceTx) EndpointGroup(ID portainer.EndpointGroupID) (*portainer.EndpointGroup, error) {
var endpointGroup portainer.EndpointGroup
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &endpointGroup)
if err != nil {
return nil, err
}
return &endpointGroup, nil
}
// UpdateEndpointGroup updates an environment(endpoint) group.
func (service ServiceTx) UpdateEndpointGroup(ID portainer.EndpointGroupID, endpointGroup *portainer.EndpointGroup) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, endpointGroup)
}
// DeleteEndpointGroup deletes an environment(endpoint) group.
func (service ServiceTx) DeleteEndpointGroup(ID portainer.EndpointGroupID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}
// EndpointGroups return an array containing all the environment(endpoint) groups.
func (service ServiceTx) EndpointGroups() ([]portainer.EndpointGroup, error) {
var endpointGroups = make([]portainer.EndpointGroup, 0)
err := service.tx.GetAll(
BucketName,
&portainer.EndpointGroup{},
func(obj interface{}) (interface{}, error) {
endpointGroup, ok := obj.(*portainer.EndpointGroup)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointGroup object")
return nil, fmt.Errorf("failed to convert to EndpointGroup object: %s", obj)
}
endpointGroups = append(endpointGroups, *endpointGroup)
return &portainer.EndpointGroup{}, nil
})
return endpointGroups, err
}
// CreateEndpointGroup assign an ID to a new environment(endpoint) group and saves it.
func (service ServiceTx) Create(endpointGroup *portainer.EndpointGroup) error {
return service.tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
endpointGroup.ID = portainer.EndpointGroupID(id)
return int(endpointGroup.ID), endpointGroup
},
)
}

View File

@@ -4,24 +4,33 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "endpoint_relations"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "endpoint_relations"
// Service represents a service for managing environment(endpoint) relation data.
type Service struct {
connection portainer.Connection
connection portainer.Connection
updateStackFn func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
updateStackFnTx func(tx portainer.Transaction, ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
}
func (service *Service) BucketName() string {
return BucketName
}
func (service *Service) RegisterUpdateStackFunction(
updateFunc func(portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
updateFuncTx func(portainer.Transaction, portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
) {
service.updateStackFn = updateFunc
service.updateStackFnTx = updateFuncTx
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
@@ -34,6 +43,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// EndpointRelations returns an array of all EndpointRelations
func (service *Service) EndpointRelations() ([]portainer.EndpointRelation, error) {
var all = make([]portainer.EndpointRelation, 0)
@@ -71,17 +87,105 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
// CreateEndpointRelation saves endpointRelation
func (service *Service) Create(endpointRelation *portainer.EndpointRelation) error {
return service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
cache.Del(endpointRelation.EndpointID)
return err
}
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
func (service *Service) UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
identifier := service.connection.ConvertToKey(int(EndpointID))
return service.connection.UpdateObject(BucketName, identifier, endpointRelation)
func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
previousRelationState, _ := service.EndpointRelation(endpointID)
identifier := service.connection.ConvertToKey(int(endpointID))
err := service.connection.UpdateObject(BucketName, identifier, endpointRelation)
cache.Del(endpointID)
if err != nil {
return err
}
updatedRelationState, _ := service.EndpointRelation(endpointID)
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
return nil
}
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
func (service *Service) DeleteEndpointRelation(EndpointID portainer.EndpointID) error {
identifier := service.connection.ConvertToKey(int(EndpointID))
return service.connection.DeleteObject(BucketName, identifier)
func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
deletedRelation, _ := service.EndpointRelation(endpointID)
identifier := service.connection.ConvertToKey(int(endpointID))
err := service.connection.DeleteObject(BucketName, identifier)
cache.Del(endpointID)
if err != nil {
return err
}
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
return nil
}
func (service *Service) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
rels, err := service.EndpointRelations()
if err != nil {
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
return
}
for _, rel := range rels {
for id := range rel.EdgeStacks {
if edgeStackID == id {
cache.Del(rel.EndpointID)
}
}
}
}
func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
relations, _ := service.EndpointRelations()
stacksToUpdate := map[portainer.EdgeStackID]bool{}
if previousRelationState != nil {
for stackId, enabled := range previousRelationState.EdgeStacks {
// flag stack for update if stack is not in the updated relation state
// = stack has been removed for this relation
// or this relation has been deleted
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
stacksToUpdate[stackId] = true
}
}
}
if updatedRelationState != nil {
for stackId, enabled := range updatedRelationState.EdgeStacks {
// flag stack for update if stack is not in the previous relation state
// = stack has been added for this relation
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
stacksToUpdate[stackId] = true
}
}
}
// for each stack referenced by the updated relation
// list how many time this stack is referenced in all relations
// in order to update the stack deployments count
for refStackId, refStackEnabled := range stacksToUpdate {
if refStackEnabled {
numDeployments := 0
for _, r := range relations {
for sId, enabled := range r.EdgeStacks {
if enabled && sId == refStackId {
numDeployments += 1
}
}
}
service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
})
}
}
}

View File

@@ -0,0 +1,159 @@
package endpointrelation
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// EndpointRelations returns an array of all EndpointRelations
func (service ServiceTx) EndpointRelations() ([]portainer.EndpointRelation, error) {
var all = make([]portainer.EndpointRelation, 0)
err := service.tx.GetAll(
BucketName,
&portainer.EndpointRelation{},
func(obj interface{}) (interface{}, error) {
r, ok := obj.(*portainer.EndpointRelation)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointRelation object")
return nil, fmt.Errorf("failed to convert to EndpointRelation object: %s", obj)
}
all = append(all, *r)
return &portainer.EndpointRelation{}, nil
})
return all, err
}
// EndpointRelation returns an Environment(Endpoint) relation object by EndpointID
func (service ServiceTx) EndpointRelation(endpointID portainer.EndpointID) (*portainer.EndpointRelation, error) {
var endpointRelation portainer.EndpointRelation
identifier := service.service.connection.ConvertToKey(int(endpointID))
err := service.tx.GetObject(BucketName, identifier, &endpointRelation)
if err != nil {
return nil, err
}
return &endpointRelation, nil
}
// CreateEndpointRelation saves endpointRelation
func (service ServiceTx) Create(endpointRelation *portainer.EndpointRelation) error {
err := service.tx.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
cache.Del(endpointRelation.EndpointID)
return err
}
// UpdateEndpointRelation updates an Environment(Endpoint) relation object
func (service ServiceTx) UpdateEndpointRelation(endpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error {
previousRelationState, _ := service.EndpointRelation(endpointID)
identifier := service.service.connection.ConvertToKey(int(endpointID))
err := service.tx.UpdateObject(BucketName, identifier, endpointRelation)
cache.Del(endpointID)
if err != nil {
return err
}
updatedRelationState, _ := service.EndpointRelation(endpointID)
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
return nil
}
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
deletedRelation, _ := service.EndpointRelation(endpointID)
identifier := service.service.connection.ConvertToKey(int(endpointID))
err := service.tx.DeleteObject(BucketName, identifier)
cache.Del(endpointID)
if err != nil {
return err
}
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
return nil
}
func (service ServiceTx) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
rels, err := service.EndpointRelations()
if err != nil {
log.Error().Err(err).Msg("cannot retrieve endpoint relations")
return
}
for _, rel := range rels {
for id := range rel.EdgeStacks {
if edgeStackID == id {
cache.Del(rel.EndpointID)
}
}
}
}
func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
relations, _ := service.EndpointRelations()
stacksToUpdate := map[portainer.EdgeStackID]bool{}
if previousRelationState != nil {
for stackId, enabled := range previousRelationState.EdgeStacks {
// flag stack for update if stack is not in the updated relation state
// = stack has been removed for this relation
// or this relation has been deleted
if enabled && (updatedRelationState == nil || !updatedRelationState.EdgeStacks[stackId]) {
stacksToUpdate[stackId] = true
}
}
}
if updatedRelationState != nil {
for stackId, enabled := range updatedRelationState.EdgeStacks {
// flag stack for update if stack is not in the previous relation state
// = stack has been added for this relation
if enabled && (previousRelationState == nil || !previousRelationState.EdgeStacks[stackId]) {
stacksToUpdate[stackId] = true
}
}
}
// for each stack referenced by the updated relation
// list how many time this stack is referenced in all relations
// in order to update the stack deployments count
for refStackId, refStackEnabled := range stacksToUpdate {
if refStackEnabled {
numDeployments := 0
for _, r := range relations {
for sId, enabled := range r.EdgeStacks {
if enabled && sId == refStackId {
numDeployments += 1
}
}
}
service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
})
}
}
}

View File

@@ -4,7 +4,8 @@ import "errors"
var (
// TODO: i'm pretty sure this needs wrapping at several levels
ErrObjectNotFound = errors.New("object not found inside the database")
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrDBImportFailed = errors.New("importing backup failed")
ErrObjectNotFound = errors.New("object not found inside the database")
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrDBImportFailed = errors.New("importing backup failed")
ErrDatabaseIsUpdating = errors.New("database is currently in updating state. Failed prior upgrade. Please restore from backup or delete the database and restart Portainer")
)

View File

@@ -6,29 +6,19 @@ import (
"io"
"time"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/edgetypes"
portainer "github.com/portainer/portainer/api"
)
type (
// DataStore defines the interface to manage the data
DataStore interface {
Open() (newStore bool, err error)
Init() error
Close() error
MigrateData() error
Rollback(force bool) error
CheckCurrentEdition() error
BackupTo(w io.Writer) error
Export(filename string) (err error)
DataStoreTx interface {
IsErrObjectNotFound(err error) bool
CustomTemplate() CustomTemplateService
EdgeGroup() EdgeGroupService
EdgeJob() EdgeJobService
EdgeStack() EdgeStackService
EdgeUpdateSchedule() EdgeUpdateScheduleService
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
@@ -51,6 +41,22 @@ type (
Webhook() WebhookService
}
// DataStore defines the interface to manage the data
DataStore interface {
Open() (newStore bool, err error)
Init() error
Close() error
UpdateTx(func(DataStoreTx) error) error
ViewTx(func(DataStoreTx) error) error
MigrateData() error
Rollback(force bool) error
CheckCurrentEdition() error
BackupTo(w io.Writer) error
Export(filename string) (err error)
DataStoreTx
}
// CustomTemplateService represents a service to manage custom templates
CustomTemplateService interface {
GetNextIdentifier() int
@@ -68,6 +74,7 @@ type (
EdgeGroup(ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error)
Create(group *portainer.EdgeGroup) error
UpdateEdgeGroup(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error
UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFunc func(group *portainer.EdgeGroup)) error
DeleteEdgeGroup(ID portainer.EdgeGroupID) error
BucketName() string
}
@@ -78,28 +85,20 @@ type (
EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error)
Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
UpdateEdgeJobFunc(ID portainer.EdgeJobID, updateFunc func(edgeJob *portainer.EdgeJob)) error
DeleteEdgeJob(ID portainer.EdgeJobID) error
GetNextIdentifier() int
BucketName() string
}
EdgeUpdateScheduleService interface {
ActiveSchedule(environmentID portainer.EndpointID) *edgetypes.EndpointUpdateScheduleRelation
ActiveSchedules(environmentIDs []portainer.EndpointID) []edgetypes.EndpointUpdateScheduleRelation
List() ([]edgetypes.UpdateSchedule, error)
Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error)
Create(edgeUpdateSchedule *edgetypes.UpdateSchedule) error
Update(ID edgetypes.UpdateScheduleID, edgeUpdateSchedule *edgetypes.UpdateSchedule) error
Delete(ID edgetypes.UpdateScheduleID) error
BucketName() string
}
// EdgeStackService represents a service to manage Edge stacks
EdgeStackService interface {
EdgeStacks() ([]portainer.EdgeStack, error)
EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error
DeleteEdgeStack(ID portainer.EdgeStackID) error
GetNextIdentifier() int
BucketName() string
@@ -108,6 +107,9 @@ type (
// EndpointService represents a service for managing environment(endpoint) data
EndpointService interface {
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
Heartbeat(endpointID portainer.EndpointID) (int64, bool)
UpdateHeartbeat(endpointID portainer.EndpointID)
Endpoints() ([]portainer.Endpoint, error)
Create(endpoint *portainer.Endpoint) error
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
@@ -210,7 +212,6 @@ type (
SettingsService interface {
Settings() (*portainer.Settings, error)
UpdateSettings(settings *portainer.Settings) error
IsFeatureFlagEnabled(feature portainer.Feature) bool
BucketName() string
}
@@ -251,6 +252,7 @@ type (
Tag(ID portainer.TagID) (*portainer.Tag, error)
Create(tag *portainer.Tag) error
UpdateTag(ID portainer.TagID, tag *portainer.Tag) error
UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error
DeleteTag(ID portainer.TagID) error
BucketName() string
}
@@ -278,6 +280,7 @@ type (
DeleteTeamMembershipByUserID(userID portainer.UserID) error
DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error
BucketName() string
DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.TeamID, userID portainer.UserID) error
}
// TunnelServerService represents a service for managing data associated to the tunnel server
@@ -301,12 +304,11 @@ type (
// VersionService represents a service for managing version data
VersionService interface {
DBVersion() (int, error)
Edition() (portainer.SoftwareEdition, error)
InstanceID() (string, error)
StoreDBVersion(version int) error
StoreInstanceID(ID string) error
BucketName() string
UpdateInstanceID(ID string) error
Version() (*models.Version, error)
UpdateVersion(*models.Version) error
}
// WebhookService represents a service for managing webhook data.

View File

@@ -8,10 +8,8 @@ import (
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "registries"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "registries"
// Service represents a service for managing environment(endpoint) data.
type Service struct {
@@ -34,7 +32,14 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
// Registry returns an registry by ID.
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// Registry returns a registry by ID.
func (service *Service) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
var registry portainer.Registry
identifier := service.connection.ConvertToKey(int(ID))
@@ -69,7 +74,7 @@ func (service *Service) Registries() ([]portainer.Registry, error) {
return registries, err
}
// CreateRegistry creates a new registry.
// Create creates a new registry.
func (service *Service) Create(registry *portainer.Registry) error {
return service.connection.CreateObject(
BucketName,
@@ -80,13 +85,13 @@ func (service *Service) Create(registry *portainer.Registry) error {
)
}
// UpdateRegistry updates an registry.
// UpdateRegistry updates a registry.
func (service *Service) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, registry)
}
// DeleteRegistry deletes an registry.
// DeleteRegistry deletes a registry.
func (service *Service) DeleteRegistry(ID portainer.RegistryID) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)

View File

@@ -0,0 +1,75 @@
package registry
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// Registry returns a registry by ID.
func (service ServiceTx) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
var registry portainer.Registry
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &registry)
if err != nil {
return nil, err
}
return &registry, nil
}
// Registries returns an array containing all the registries.
func (service ServiceTx) Registries() ([]portainer.Registry, error) {
var registries = make([]portainer.Registry, 0)
err := service.tx.GetAll(
BucketName,
&portainer.Registry{},
func(obj interface{}) (interface{}, error) {
registry, ok := obj.(*portainer.Registry)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Registry object")
return nil, fmt.Errorf("Failed to convert to Registry object: %s", obj)
}
registries = append(registries, *registry)
return &portainer.Registry{}, nil
})
return registries, err
}
// Create creates a new registry.
func (service ServiceTx) Create(registry *portainer.Registry) error {
return service.tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
registry.ID = portainer.RegistryID(id)
return int(registry.ID), registry
},
)
}
// UpdateRegistry updates a registry.
func (service ServiceTx) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, registry)
}
// DeleteRegistry deletes a registry.
func (service ServiceTx) DeleteRegistry(ID portainer.RegistryID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}

View File

@@ -47,17 +47,3 @@ func (service *Service) Settings() (*portainer.Settings, error) {
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
}
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
settings, err := service.Settings()
if err != nil {
return false
}
featureFlagSetting, ok := settings.FeatureFlagSettings[feature]
if ok {
return featureFlagSetting
}
return false
}

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -30,6 +31,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
func (service *Service) Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) {
var snapshot portainer.Snapshot
identifier := service.connection.ConvertToKey(int(endpointID))
@@ -51,7 +59,7 @@ func (service *Service) Snapshots() ([]portainer.Snapshot, error) {
func(obj interface{}) (interface{}, error) {
snapshot, ok := obj.(*portainer.Snapshot)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Snapshot object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Snapshot object")
return nil, fmt.Errorf("failed to convert to Snapshot object: %s", obj)
}
snapshots = append(snapshots, *snapshot)

View File

@@ -0,0 +1,63 @@
package snapshot
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
func (service ServiceTx) Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) {
var snapshot portainer.Snapshot
identifier := service.service.connection.ConvertToKey(int(endpointID))
err := service.tx.GetObject(BucketName, identifier, &snapshot)
if err != nil {
return nil, err
}
return &snapshot, nil
}
func (service ServiceTx) Snapshots() ([]portainer.Snapshot, error) {
var snapshots = make([]portainer.Snapshot, 0)
err := service.tx.GetAllWithJsoniter(
BucketName,
&portainer.Snapshot{},
func(obj interface{}) (interface{}, error) {
snapshot, ok := obj.(*portainer.Snapshot)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Snapshot object")
return nil, fmt.Errorf("failed to convert to Snapshot object: %s", obj)
}
snapshots = append(snapshots, *snapshot)
return &portainer.Snapshot{}, nil
})
return snapshots, err
}
func (service ServiceTx) UpdateSnapshot(snapshot *portainer.Snapshot) error {
identifier := service.service.connection.ConvertToKey(int(snapshot.EndpointID))
return service.tx.UpdateObject(BucketName, identifier, snapshot)
}
func (service ServiceTx) DeleteSnapshot(endpointID portainer.EndpointID) error {
identifier := service.service.connection.ConvertToKey(int(endpointID))
return service.tx.DeleteObject(BucketName, identifier)
}
func (service ServiceTx) Create(snapshot *portainer.Snapshot) error {
return service.tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
}

View File

@@ -68,13 +68,13 @@ func (b *stackBuilder) createNewStack(webhookID string) portainer.Stack {
if webhookID == "" {
if b.count%2 == 0 {
stack.AutoUpdate = &portainer.StackAutoUpdate{
stack.AutoUpdate = &portainer.AutoUpdateSettings{
Interval: "",
Webhook: "",
}
} // else keep AutoUpdate nil
} else {
stack.AutoUpdate = &portainer.StackAutoUpdate{Webhook: webhookID}
stack.AutoUpdate = &portainer.AutoUpdateSettings{Webhook: webhookID}
}
err := b.store.StackService.Create(&stack)
@@ -91,8 +91,8 @@ func Test_RefreshableStacks(t *testing.T) {
defer teardown()
staticStack := portainer.Stack{ID: 1}
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.StackAutoUpdate{Webhook: "webhook"}}
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.StackAutoUpdate{Interval: "1m"}}
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.AutoUpdateSettings{Webhook: "webhook"}}
refreshableStack := portainer.Stack{ID: 3, AutoUpdate: &portainer.AutoUpdateSettings{Interval: "1m"}}
for _, stack := range []*portainer.Stack{&staticStack, &stackWithWebhook, &refreshableStack} {
err := store.Stack().Create(stack)

View File

@@ -34,6 +34,13 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// Tags return an array containing all the tags.
func (service *Service) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0)
@@ -80,12 +87,22 @@ func (service *Service) Create(tag *portainer.Tag) error {
)
}
// UpdateTag updates a tag.
// Deprecated: Use UpdateTagFunc instead.
func (service *Service) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, tag)
}
// UpdateTagFunc updates a tag inside a transaction avoiding data races.
func (service *Service) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error {
id := service.connection.ConvertToKey(int(ID))
tag := &portainer.Tag{}
return service.connection.UpdateObjectFunc(BucketName, id, tag, func() {
updateFunc(tag)
})
}
// DeleteTag deletes a tag.
func (service *Service) DeleteTag(ID portainer.TagID) error {
identifier := service.connection.ConvertToKey(int(ID))

View File

@@ -0,0 +1,82 @@
package tag
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// Tags return an array containing all the tags.
func (service ServiceTx) Tags() ([]portainer.Tag, error) {
var tags = make([]portainer.Tag, 0)
err := service.tx.GetAll(
BucketName,
&portainer.Tag{},
func(obj interface{}) (interface{}, error) {
tag, ok := obj.(*portainer.Tag)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Tag object")
return nil, fmt.Errorf("failed to convert to Tag object: %s", obj)
}
tags = append(tags, *tag)
return &portainer.Tag{}, nil
})
return tags, err
}
// Tag returns a tag by ID.
func (service ServiceTx) Tag(ID portainer.TagID) (*portainer.Tag, error) {
var tag portainer.Tag
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &tag)
if err != nil {
return nil, err
}
return &tag, nil
}
// CreateTag creates a new tag.
func (service ServiceTx) Create(tag *portainer.Tag) error {
return service.tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
tag.ID = portainer.TagID(id)
return int(tag.ID), tag
},
)
}
// UpdateTag updates a tag
func (service ServiceTx) UpdateTag(ID portainer.TagID, tag *portainer.Tag) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.UpdateObject(BucketName, identifier, tag)
}
// UpdateTagFunc is a no-op inside a transaction
func (service ServiceTx) UpdateTagFunc(ID portainer.TagID, updateFunc func(tag *portainer.Tag)) error {
return errors.New("cannot be called inside a transaction")
}
// DeleteTag deletes a tag.
func (service ServiceTx) DeleteTag(ID portainer.TagID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
return service.tx.DeleteObject(BucketName, identifier)
}

View File

@@ -144,6 +144,7 @@ func (service *Service) DeleteTeamMembership(ID portainer.TeamMembershipID) erro
func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
return service.connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
@@ -164,6 +165,7 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
return service.connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
@@ -179,3 +181,23 @@ func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) er
return -1, false
})
}
func (service *Service) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.TeamID, userID portainer.UserID) error {
return service.connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
return -1, false
}
if membership.TeamID == teamID && membership.UserID == userID {
return int(membership.ID), true
}
return -1, false
})
}

View File

@@ -1,17 +1,18 @@
package version
import (
"strconv"
"errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "version"
versionKey = "DB_VERSION"
instanceKey = "INSTANCE_ID"
editionKey = "EDITION"
versionKey = "VERSION"
updatingKey = "DB_UPDATING"
)
@@ -20,10 +21,6 @@ type Service struct {
connection portainer.Connection
}
func (service *Service) BucketName() string {
return BucketName
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
@@ -36,56 +33,87 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
// DBVersion retrieves the stored database version.
func (service *Service) DBVersion() (int, error) {
var version string
err := service.connection.GetObject(BucketName, []byte(versionKey), &version)
func (service *Service) SchemaVersion() (string, error) {
v, err := service.Version()
if err != nil {
return 0, err
return "", err
}
return strconv.Atoi(version)
return v.SchemaVersion, nil
}
func (service *Service) UpdateSchemaVersion(version string) error {
v, err := service.Version()
if err != nil {
return err
}
v.SchemaVersion = version
return service.UpdateVersion(v)
}
// Edition retrieves the stored portainer edition.
func (service *Service) Edition() (portainer.SoftwareEdition, error) {
var edition string
err := service.connection.GetObject(BucketName, []byte(editionKey), &edition)
v, err := service.Version()
if err != nil {
return 0, err
}
e, err := strconv.Atoi(edition)
if err != nil {
return 0, err
}
return portainer.SoftwareEdition(e), nil
}
// StoreDBVersion store the database version.
func (service *Service) StoreDBVersion(version int) error {
return service.connection.UpdateObject(BucketName, []byte(versionKey), strconv.Itoa(version))
return portainer.SoftwareEdition(v.Edition), nil
}
// IsUpdating retrieves the database updating status.
func (service *Service) IsUpdating() (bool, error) {
var isUpdating bool
err := service.connection.GetObject(BucketName, []byte(updatingKey), &isUpdating)
if err != nil && errors.Is(err, dserrors.ErrObjectNotFound) {
return false, nil
}
return isUpdating, err
}
// StoreIsUpdating store the database updating status.
func (service *Service) StoreIsUpdating(isUpdating bool) error {
return service.connection.UpdateObject(BucketName, []byte(updatingKey), isUpdating)
return service.connection.DeleteObject(BucketName, []byte(updatingKey))
}
// InstanceID retrieves the stored instance ID.
func (service *Service) InstanceID() (string, error) {
var id string
err := service.connection.GetObject(BucketName, []byte(instanceKey), &id)
return id, err
v, err := service.Version()
if err != nil {
return "", err
}
return v.InstanceID, nil
}
// StoreInstanceID store the instance ID.
func (service *Service) StoreInstanceID(ID string) error {
return service.connection.UpdateObject(BucketName, []byte(instanceKey), ID)
func (service *Service) UpdateInstanceID(id string) error {
v, err := service.Version()
if err != nil {
if !dataservices.IsErrObjectNotFound(err) {
return err
}
v = &models.Version{}
}
v.InstanceID = id
return service.UpdateVersion(v)
}
// Version retrieve the version object.
func (service *Service) Version() (*models.Version, error) {
var v models.Version
err := service.connection.GetObject(BucketName, []byte(versionKey), &v)
if err != nil {
return nil, err
}
return &v, nil
}
// UpdateVersion persists a Version object.
func (service *Service) UpdateVersion(version *models.Version) error {
return service.connection.UpdateObject(BucketName, []byte(versionKey), version)
}

View File

@@ -6,6 +6,7 @@ import (
"path"
"time"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log"
)
@@ -53,7 +54,7 @@ func (store *Store) copyDBFile(from string, to string) error {
// BackupOptions provide a helper to inject backup options
type BackupOptions struct {
Version int // I can't find this used for anything other than a filename
Version string
BackupDir string
BackupFileName string
BackupPath string
@@ -70,26 +71,32 @@ func getBackupRestoreOptions(backupDir string) *BackupOptions {
}
// Backup current database with default options
func (store *Store) Backup() (string, error) {
return store.backupWithOptions(nil)
func (store *Store) Backup(version *models.Version) (string, error) {
if version == nil {
return store.backupWithOptions(nil)
}
return store.backupWithOptions(&BackupOptions{
Version: version.SchemaVersion,
})
}
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
if options == nil {
options = &BackupOptions{}
}
if options.Version == 0 {
version, err := store.version()
if options.Version == "" {
v, err := store.VersionService.Version()
if err != nil {
version = 0
options.Version = ""
}
options.Version = version
options.Version = v.SchemaVersion
}
if options.BackupDir == "" {
options.BackupDir = store.commonBackupDir()
}
if options.BackupFileName == "" {
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), options.Version, time.Now().Format("20060102150405"))
}
if options.BackupPath == "" {
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
@@ -168,7 +175,6 @@ func (store *Store) removeWithOptions(options *BackupOptions) error {
if os.IsNotExist(err) {
log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to remove does not exist")
return err
}
@@ -176,7 +182,6 @@ func (store *Store) removeWithOptions(options *BackupOptions) error {
err = os.Remove(options.BackupPath)
if err != nil {
log.Error().Err(err).Msg("failed")
return err
}

View File

@@ -7,10 +7,11 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
)
func TestCreateBackupFolders(t *testing.T) {
_, store, teardown := MustNewTestStore(t, false, true)
_, store, teardown := MustNewTestStore(t, true, true)
defer teardown()
connection := store.GetConnection()
@@ -45,10 +46,13 @@ func TestBackup(t *testing.T) {
defer teardown()
t.Run("Backup should create default db backup", func(t *testing.T) {
store.VersionService.StoreDBVersion(portainer.DBVersion)
v := models.Version{
SchemaVersion: portainer.APIVersion,
}
store.VersionService.UpdateVersion(&v)
store.backupWithOptions(nil)
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%03d.*", portainer.DBVersion))
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%s.*", portainer.APIVersion))
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}

View File

@@ -8,27 +8,12 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
portainerErrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
func (store *Store) version() (int, error) {
version, err := store.VersionService.DBVersion()
if store.IsErrObjectNotFound(err) {
version = 0
}
return version, err
}
func (store *Store) edition() portainer.SoftwareEdition {
edition, err := store.VersionService.Edition()
if store.IsErrObjectNotFound(err) {
edition = portainer.PortainerCE
}
return edition
}
// NewStore initializes a new Store and the associated services
func NewStore(storePath string, fileService portainer.FileService, connection portainer.Connection) *Store {
return &Store{
@@ -39,8 +24,6 @@ func NewStore(storePath string, fileService portainer.FileService, connection po
// Open opens and initializes the BoltDB database.
func (store *Store) Open() (newStore bool, err error) {
newStore = true
encryptionReq, err := store.connection.NeedsEncryptionMigration()
if err != nil {
return false, err
@@ -55,37 +38,48 @@ func (store *Store) Open() (newStore bool, err error) {
err = store.connection.Open()
if err != nil {
return newStore, err
return false, err
}
err = store.initServices()
if err != nil {
return newStore, err
return false, err
}
// if we have DBVersion in the database then ensure we flag this as NOT a new store
version, err := store.VersionService.DBVersion()
// If no settings object exists then assume we have a new store
_, err = store.SettingsService.Settings()
if err != nil {
if store.IsErrObjectNotFound(err) {
return newStore, nil
return true, nil
}
return newStore, err
return false, err
}
if version > 0 {
log.Debug().Int("version", version).Msg("opened existing store")
return false, nil
}
return newStore, nil
return false, nil
}
func (store *Store) Close() error {
return store.connection.Close()
}
func (store *Store) UpdateTx(fn func(dataservices.DataStoreTx) error) error {
return store.connection.UpdateTx(func(tx portainer.Transaction) error {
return fn(&StoreTx{
store: store,
tx: tx,
})
})
}
func (store *Store) ViewTx(fn func(dataservices.DataStoreTx) error) error {
return store.connection.ViewTx(func(tx portainer.Transaction) error {
return fn(&StoreTx{
store: store,
tx: tx,
})
})
}
// BackupTo backs up db to a provided writer.
// It does hot backup and doesn't block other database reads and writes
func (store *Store) BackupTo(w io.Writer) error {
@@ -94,17 +88,29 @@ func (store *Store) BackupTo(w io.Writer) error {
// CheckCurrentEdition checks if current edition is community edition
func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.PortainerCE {
if store.edition() != portainer.Edition {
return portainerErrors.ErrWrongDBEdition
}
return nil
}
func (store *Store) edition() portainer.SoftwareEdition {
edition, err := store.VersionService.Edition()
if store.IsErrObjectNotFound(err) {
edition = portainer.PortainerCE
}
return edition
}
// TODO: move the use of this to dataservices.IsErrObjectNotFound()?
func (store *Store) IsErrObjectNotFound(e error) bool {
return e == portainerErrors.ErrObjectNotFound
}
func (store *Store) Connection() portainer.Connection {
return store.connection
}
func (store *Store) Rollback(force bool) error {
return store.connectionRollback(force)
}

View File

@@ -1,18 +1,12 @@
package datastore
import (
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
)
// Init creates the default data set.
func (store *Store) Init() error {
err := store.checkOrCreateInstanceID()
if err != nil {
return err
}
err = store.checkOrCreateDefaultSettings()
err := store.checkOrCreateDefaultSettings()
if err != nil {
return err
}
@@ -25,21 +19,6 @@ func (store *Store) Init() error {
return store.checkOrCreateDefaultData()
}
func (store *Store) checkOrCreateInstanceID() error {
_, err := store.VersionService.InstanceID()
if store.IsErrObjectNotFound(err) {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID := uid.String()
return store.VersionService.StoreInstanceID(instanceID)
}
return err
}
func (store *Store) checkOrCreateDefaultSettings() error {
// TODO: these need to also be applied when importing
settings, err := store.SettingsService.Settings()

View File

@@ -4,37 +4,72 @@ import (
"fmt"
"runtime/debug"
portainer "github.com/portainer/portainer/api"
portaineree "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/database/models"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization"
werrors "github.com/pkg/errors"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
func (store *Store) MigrateData() error {
version, err := store.version()
updating, err := store.VersionService.IsUpdating()
if err != nil {
return errors.Wrap(err, "while checking if the store is updating")
}
if updating {
return dserrors.ErrDatabaseIsUpdating
}
// migrate new version bucket if required (doesn't write anything to db yet)
version, err := store.getOrMigrateLegacyVersion()
if err != nil {
return errors.Wrap(err, "while migrating legacy version")
}
migratorParams := store.newMigratorParameters(version)
migrator := migrator.NewMigrator(migratorParams)
if !migrator.NeedsMigration() {
return nil
}
// before we alter anything in the DB, create a backup
backupPath, err := store.Backup(version)
if err != nil {
return errors.Wrap(err, "while backing up database")
}
err = store.FailSafeMigrate(migrator, version)
if err != nil {
err = errors.Wrap(err, "failed to migrate database")
log.Warn().Msg("migration failed, restoring database to previous version")
err = store.restoreWithOptions(&BackupOptions{BackupPath: backupPath})
if err != nil {
return errors.Wrap(err, "failed to restore database")
}
log.Info().Msg("database restored to previous version")
return err
}
// Backup Database
backupPath, err := store.Backup()
if err != nil {
return werrors.Wrap(err, "while backing up db before migration")
}
return nil
}
migratorParams := &migrator.MigratorParameters{
DatabaseVersion: version,
func (store *Store) newMigratorParameters(version *models.Version) *migrator.MigratorParameters {
return &migrator.MigratorParameters{
CurrentDBVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
FDOProfilesService: store.FDOProfilesService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
@@ -49,99 +84,43 @@ func (store *Store) MigrateData() error {
FileService: store.fileService,
DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store),
EdgeStackService: store.EdgeStackService,
EdgeJobService: store.EdgeJobService,
}
// restore on error
err = store.connectionMigrateData(migratorParams)
if err != nil {
log.Error().Err(err).Msg("while DB migration, restoring DB")
// Restore options
options := BackupOptions{
BackupPath: backupPath,
}
err := store.restoreWithOptions(&options)
if err != nil {
log.Fatal().
Str("database_file", store.databasePath()).
Str("backup", options.BackupPath).Err(err).
Msg("failed restoring the backup, Portainer database file needs to restored manually by replacing the database file with a recent backup")
}
}
return err
}
// FailSafeMigrate backup and restore DB if migration fail
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) {
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator, version *models.Version) (err error) {
defer func() {
if e := recover(); e != nil {
store.Rollback(true)
// return error with cause and stacktrace (recover() doesn't include a stacktrace)
err = fmt.Errorf("%v %s", e, string(debug.Stack()))
}
}()
// !Important: we must use a named return value in the function definition and not a local
// !variable referenced from the closure or else the return value will be incorrectly set
return migrator.Migrate()
}
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
// if force is true, then migrate regardless.
func (store *Store) connectionMigrateData(migratorParams *migrator.MigratorParameters) error {
migrator := migrator.NewMigrator(migratorParams)
// backup db file before upgrading DB to support rollback
isUpdating, err := migratorParams.VersionService.IsUpdating()
if err != nil && err != errors.ErrObjectNotFound {
return err
}
if !isUpdating && migrator.Version() != portainer.DBVersion {
err = store.backupVersion(migrator)
if err != nil {
return werrors.Wrapf(err, "failed to backup database")
}
}
if migrator.Version() < portainer.DBVersion {
log.Info().
Int("migrator_version", migrator.Version()).
Int("db_version", portainer.DBVersion).
Msg("migrating database")
err = store.FailSafeMigrate(migrator)
if err != nil {
log.Error().Err(err).Msg("an error occurred during database migration")
return err
}
}
return nil
}
// backupVersion will backup the database or panic if any errors occur
func (store *Store) backupVersion(migrator *migrator.Migrator) error {
log.Info().Msg("backing up database prior to version upgrade")
options := getBackupRestoreOptions(store.commonBackupDir())
_, err := store.backupWithOptions(options)
err = store.VersionService.StoreIsUpdating(true)
if err != nil {
log.Error().Err(err).Msg("an error occurred during database backup")
return errors.Wrap(err, "while updating the store")
}
removalErr := store.removeWithOptions(options)
if removalErr != nil {
log.Error().Err(err).Msg("an error occurred during store removal prior to backup")
}
// now update the version to the new struct (if required)
err = store.finishMigrateLegacyVersion(version)
if err != nil {
return errors.Wrap(err, "while updating version")
}
log.Info().Msg("migrating database from version " + version.SchemaVersion + " to " + portaineree.APIVersion)
err = migrator.Migrate()
if err != nil {
return err
}
err = store.VersionService.StoreIsUpdating(false)
if err != nil {
return errors.Wrap(err, "failed to update the store")
}
return nil
}

View File

@@ -10,39 +10,41 @@ import (
"strings"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/google/go-cmp/cmp"
"github.com/portainer/portainer/api/database/models"
"github.com/rs/zerolog/log"
)
// testVersion is a helper which tests current store version against wanted version
func testVersion(store *Store, versionWant int, t *testing.T) {
v, err := store.VersionService.DBVersion()
func testVersion(store *Store, versionWant string, t *testing.T) {
v, err := store.VersionService.Version()
if err != nil {
t.Errorf("Expect store version to be %d but was %d with error: %s", versionWant, v, err)
t.Errorf("Expect store version to be %s but was %s with error: %s", versionWant, v.SchemaVersion, err)
}
if v != versionWant {
t.Errorf("Expect store version to be %d but was %d", versionWant, v)
if v.SchemaVersion != versionWant {
t.Errorf("Expect store version to be %s but was %s", versionWant, v.SchemaVersion)
}
}
func TestMigrateData(t *testing.T) {
snapshotTests := []struct {
testName string
srcPath string
wantPath string
testName string
srcPath string
wantPath string
overrideInstanceId bool
}{
{
testName: "migrate version 24 to latest",
srcPath: "test_data/input_24.json",
wantPath: "test_data/output_24_to_latest.json",
testName: "migrate version 24 to latest",
srcPath: "test_data/input_24.json",
wantPath: "test_data/output_24_to_latest.json",
overrideInstanceId: true,
},
}
for _, test := range snapshotTests {
t.Run(test.testName, func(t *testing.T) {
err := migrateDBTestHelper(t, test.srcPath, test.wantPath)
err := migrateDBTestHelper(t, test.srcPath, test.wantPath, test.overrideInstanceId)
if err != nil {
t.Errorf(
"Failed migrating mock database %v: %v",
@@ -53,111 +55,111 @@ func TestMigrateData(t *testing.T) {
})
}
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
newStore, store, teardown := MustNewTestStore(t, false, true)
defer teardown()
// t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
// newStore, store, teardown := MustNewTestStore(t, true, false)
// defer teardown()
if !newStore {
t.Error("Expect a new DB")
}
// if !newStore {
// t.Error("Expect a new DB")
// }
// not called for new stores
//store.MigrateData()
// testVersion(store, portainer.APIVersion, t)
// store.Close()
testVersion(store, portainer.DBVersion, t)
store.Close()
// newStore, _ = store.Open()
// if newStore {
// t.Error("Expect store to NOT be new DB")
// }
// })
newStore, _ = store.Open()
if newStore {
t.Error("Expect store to NOT be new DB")
}
})
// tests := []struct {
// version string
// expectedVersion string
// }{
// {version: "1.24.1", expectedVersion: portainer.APIVersion},
// {version: "2.0.0", expectedVersion: portainer.APIVersion},
// }
// for _, tc := range tests {
// _, store, teardown := MustNewTestStore(t, true, true)
// defer teardown()
tests := []struct {
version int
expectedVersion int
}{
{version: 17, expectedVersion: portainer.DBVersion},
{version: 21, expectedVersion: portainer.DBVersion},
}
for _, tc := range tests {
_, store, teardown := MustNewTestStore(t, true, true)
defer teardown()
// // Setup data
// v := models.Version{SchemaVersion: tc.version}
// store.VersionService.UpdateVersion(&v)
// Setup data
store.VersionService.StoreDBVersion(tc.version)
// // Required roles by migrations 22.2
// store.RoleService.Create(&portainer.Role{ID: 1})
// store.RoleService.Create(&portainer.Role{ID: 2})
// store.RoleService.Create(&portainer.Role{ID: 3})
// store.RoleService.Create(&portainer.Role{ID: 4})
// Required roles by migrations 22.2
store.RoleService.Create(&portainer.Role{ID: 1})
store.RoleService.Create(&portainer.Role{ID: 2})
store.RoleService.Create(&portainer.Role{ID: 3})
store.RoleService.Create(&portainer.Role{ID: 4})
// t.Run(fmt.Sprintf("MigrateData for version %s", tc.version), func(t *testing.T) {
// store.MigrateData()
// testVersion(store, tc.expectedVersion, t)
// })
t.Run(fmt.Sprintf("MigrateData for version %d", tc.version), func(t *testing.T) {
store.MigrateData()
testVersion(store, tc.expectedVersion, t)
})
// t.Run(fmt.Sprintf("Restoring DB after migrateData for version %s", tc.version), func(t *testing.T) {
// store.Rollback(true)
// store.Open()
// testVersion(store, tc.version, t)
// })
// }
t.Run(fmt.Sprintf("Restoring DB after migrateData for version %d", tc.version), func(t *testing.T) {
store.Rollback(true)
store.Open()
testVersion(store, tc.version, t)
})
}
// t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
// _, store, teardown := MustNewTestStore(t, false, true)
// defer teardown()
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
_, store, teardown := MustNewTestStore(t, false, true)
defer teardown()
// v := models.Version{SchemaVersion: "1.24.1"}
// store.VersionService.UpdateVersion(&v)
version := 17
store.VersionService.StoreDBVersion(version)
// store.MigrateData()
store.MigrateData()
// testVersion(store, v.SchemaVersion, t)
// })
testVersion(store, version, t)
})
// t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
// _, store, teardown := MustNewTestStore(t, false, true)
// defer teardown()
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
_, store, teardown := MustNewTestStore(t, false, true)
defer teardown()
store.VersionService.StoreDBVersion(0)
// v := models.Version{SchemaVersion: "0.0.0"}
// store.VersionService.UpdateVersion(&v)
store.MigrateData()
// store.MigrateData()
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
if !isFileExist(options.BackupPath) {
t.Errorf("Backup file should exist; file=%s", options.BackupPath)
}
})
// if !isFileExist(options.BackupPath) {
// t.Errorf("Backup file should exist; file=%s", options.BackupPath)
// }
// })
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
_, store, teardown := MustNewTestStore(t, false, true)
defer teardown()
// t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
// _, store, teardown := MustNewTestStore(t, false, true)
// defer teardown()
store.VersionService.StoreIsUpdating(true)
// store.VersionService.StoreIsUpdating(true)
store.MigrateData()
// store.MigrateData()
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
if isFileExist(options.BackupPath) {
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
}
})
// if isFileExist(options.BackupPath) {
// t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
// }
// })
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
_, store, teardown := MustNewTestStore(t, false, true)
defer teardown()
// t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
// _, store, teardown := MustNewTestStore(t, false, true)
// defer teardown()
store.MigrateData()
// store.MigrateData()
options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
// options := store.setupOptions(getBackupRestoreOptions(store.commonBackupDir()))
if isFileExist(options.BackupPath) {
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
}
})
// if isFileExist(options.BackupPath) {
// t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
// }
// })
}
func Test_getBackupRestoreOptions(t *testing.T) {
@@ -179,18 +181,23 @@ func Test_getBackupRestoreOptions(t *testing.T) {
func TestRollback(t *testing.T) {
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
version := 21
_, store, teardown := MustNewTestStore(t, false, true)
version := models.Version{SchemaVersion: "2.4.0"}
_, store, teardown := MustNewTestStore(t, true, false)
defer teardown()
store.VersionService.StoreDBVersion(version)
_, err := store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
err := store.VersionService.UpdateVersion(&version)
if err != nil {
t.Errorf("Failed updating version: %v", err)
}
_, err = store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
if err != nil {
log.Fatal().Err(err).Msg("")
}
// Change the current edition
err = store.VersionService.StoreDBVersion(version + 10)
// Change the current version
version2 := models.Version{SchemaVersion: "2.6.0"}
err = store.VersionService.UpdateVersion(&version2)
if err != nil {
log.Fatal().Err(err).Msg("")
}
@@ -199,12 +206,17 @@ func TestRollback(t *testing.T) {
if err != nil {
t.Logf("Rollback failed: %s", err)
t.Fail()
return
}
store.Open()
testVersion(store, version, t)
_, err = store.Open()
if err != nil {
t.Logf("Open failed: %s", err)
t.Fail()
return
}
testVersion(store, version.SchemaVersion, t)
})
}
@@ -220,15 +232,20 @@ func isFileExist(path string) bool {
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
// parses it into a database, runs a migration on that database, and then
// compares it with an expected output database.
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanceId bool) error {
srcJSON, err := os.ReadFile(srcPath)
if err != nil {
t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
}
// Parse source json to db.
_, store, teardown := MustNewTestStore(t, true, false)
defer teardown()
// When we create a new test store, it sets its version field automatically to latest.
_, store, _ := MustNewTestStore(t, true, false)
fmt.Println("store.path=", store.GetConnection().GetDatabaseFilePath())
store.connection.DeleteObject("version", []byte("VERSION"))
// defer teardown()
err = importJSON(t, bytes.NewReader(srcJSON), store)
if err != nil {
return err
@@ -240,6 +257,21 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
return err
}
if overrideInstanceId {
// old versions of portainer did not have instance-id. Because this gets generated
// we need to override the expected output to match the expected value to pass the test
v, err := store.VersionService.Version()
if err != nil {
return err
}
v.InstanceID = "463d5c47-0ea5-4aca-85b1-405ceefee254"
err = store.VersionService.UpdateVersion(v)
if err != nil {
return err
}
}
// Assert that our database connection is using bolt so we can call
// exportJson rather than ExportRaw. The exportJson function allows us to
// strip out the metadata which we don't want for our tests.
@@ -275,7 +307,7 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
// Compare the result we got with the one we wanted.
if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
gotPath := filepath.Join(t.TempDir(), "portainer-migrator-test-fail.json")
gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
os.WriteFile(
gotPath,
gotJSON,
@@ -316,42 +348,65 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
t.Logf("failed casting %s to map[string]interface{}", k)
}
// New format db
version, ok := versions["VERSION"]
if ok {
err := con.CreateObjectWithStringId(
k,
[]byte("VERSION"),
version,
)
if err != nil {
t.Logf("failed writing VERSION in %s: %v", k, err)
}
}
// old format db
dbVersion, ok := versions["DB_VERSION"]
if !ok {
t.Logf("failed getting DB_VERSION from %s", k)
}
if ok {
numDBVersion, ok := dbVersion.(json.Number)
if !ok {
t.Logf("failed parsing DB_VERSION as json number from %s", k)
}
numDBVersion, ok := dbVersion.(json.Number)
if !ok {
t.Logf("failed parsing DB_VERSION as json number from %s", k)
}
intDBVersion, err := numDBVersion.Int64()
if err != nil {
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
}
intDBVersion, err := numDBVersion.Int64()
if err != nil {
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
}
err = con.CreateObjectWithStringId(
k,
[]byte("DB_VERSION"),
int(intDBVersion),
)
if err != nil {
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
err = con.CreateObjectWithStringId(
k,
[]byte("DB_VERSION"),
int(intDBVersion),
)
if err != nil {
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
}
}
instanceID, ok := versions["INSTANCE_ID"]
if !ok {
t.Logf("failed getting INSTANCE_ID from %s", k)
if ok {
err = con.CreateObjectWithStringId(
k,
[]byte("INSTANCE_ID"),
instanceID,
)
if err != nil {
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
}
}
err = con.CreateObjectWithStringId(
k,
[]byte("INSTANCE_ID"),
instanceID,
)
if err != nil {
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
edition, ok := versions["EDITION"]
if ok {
err = con.CreateObjectWithStringId(
k,
[]byte("EDITION"),
edition,
)
if err != nil {
t.Logf("failed writing EDITION in %s: %v", k, err)
}
}
case "dockerhub":

View File

@@ -54,7 +54,6 @@ func TestMigrateSettings(t *testing.T) {
}
m := migrator.NewMigrator(&migrator.MigratorParameters{
DatabaseVersion: 29,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,

View File

@@ -0,0 +1,115 @@
package datastore
import (
portaineree "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
)
const (
bucketName = "version"
legacyDBVersionKey = "DB_VERSION"
legacyInstanceKey = "INSTANCE_ID"
legacyEditionKey = "EDITION"
)
var dbVerToSemVerMap = map[int]string{
18: "1.21",
19: "1.22",
20: "1.22.1",
21: "1.22.2",
22: "1.23",
23: "1.24",
24: "1.24.1",
25: "2.0",
26: "2.1",
27: "2.2",
28: "2.4",
29: "2.4",
30: "2.6",
31: "2.7",
32: "2.9",
33: "2.9.1",
34: "2.10",
35: "2.9.3",
36: "2.11",
40: "2.13",
50: "2.14",
51: "2.14.1",
52: "2.14.2",
60: "2.15",
61: "2.15.1",
70: "2.16",
80: "2.17",
}
func dbVersionToSemanticVersion(dbVersion int) string {
if dbVersion < 18 {
return "1.0.0"
}
ver, ok := dbVerToSemVerMap[dbVersion]
if ok {
return ver
}
// We should always return something sensible
switch {
case dbVersion < 40:
return "2.11"
case dbVersion < 50:
return "2.13"
case dbVersion < 60:
return "2.14.2"
case dbVersion < 70:
return "2.15.1"
}
return "2.16.0"
}
// getOrMigrateLegacyVersion to new Version struct
func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) {
// Very old versions of portainer did not have a version bucket, lets set some defaults
dbVersion := 24
edition := int(portaineree.PortainerCE)
instanceId := ""
// If we already have a version key, we don't need to migrate
v, err := store.VersionService.Version()
if err == nil || !dataservices.IsErrObjectNotFound(err) {
return v, err
}
err = store.connection.GetObject(bucketName, []byte(legacyDBVersionKey), &dbVersion)
if err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
}
err = store.connection.GetObject(bucketName, []byte(legacyEditionKey), &edition)
if err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
}
err = store.connection.GetObject(bucketName, []byte(legacyInstanceKey), &instanceId)
if err != nil && !dataservices.IsErrObjectNotFound(err) {
return nil, err
}
return &models.Version{
SchemaVersion: dbVersionToSemanticVersion(dbVersion),
Edition: edition,
InstanceID: string(instanceId),
}, nil
}
// finishMigrateLegacyVersion writes the new version to the DB and removes the old version keys from the DB
func (store *Store) finishMigrateLegacyVersion(versionToWrite *models.Version) error {
err := store.VersionService.UpdateVersion(versionToWrite)
// Remove legacy keys if present
store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey))
store.connection.DeleteObject(bucketName, []byte(legacyEditionKey))
store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
return err
}

View File

@@ -0,0 +1,117 @@
package datastore
import (
"context"
"github.com/docker/docker/api/types"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/rs/zerolog/log"
)
type PostInitMigrator struct {
kubeFactory *cli.ClientFactory
dockerFactory *docker.ClientFactory
dataStore dataservices.DataStore
}
func NewPostInitMigrator(kubeFactory *cli.ClientFactory, dockerFactory *docker.ClientFactory, dataStore dataservices.DataStore) *PostInitMigrator {
return &PostInitMigrator{
kubeFactory: kubeFactory,
dockerFactory: dockerFactory,
dataStore: dataStore,
}
}
func (migrator *PostInitMigrator) PostInitMigrate() error {
if err := migrator.PostInitMigrateIngresses(); err != nil {
return err
}
migrator.PostInitMigrateGPUs()
return nil
}
func (migrator *PostInitMigrator) PostInitMigrateIngresses() error {
endpoints, err := migrator.dataStore.Endpoint().Endpoints()
if err != nil {
return err
}
for i := range endpoints {
// Early exit if we do not need to migrate!
if !endpoints[i].PostInitMigrations.MigrateIngresses {
return nil
}
err := migrator.kubeFactory.MigrateEndpointIngresses(&endpoints[i])
if err != nil {
log.Debug().Err(err).Msg("failure migrating endpoint ingresses")
}
}
return nil
}
// PostInitMigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found
// If there's an error getting the containers, we'll log it and move on
func (migrator *PostInitMigrator) PostInitMigrateGPUs() {
environments, err := migrator.dataStore.Endpoint().Endpoints()
if err != nil {
log.Err(err).Msg("failure getting endpoints")
return
}
for i := range environments {
if environments[i].Type == portainer.DockerEnvironment {
// // Early exit if we do not need to migrate!
if !environments[i].PostInitMigrations.MigrateGPUs {
return
}
// set the MigrateGPUs flag to false so we don't run this again
environments[i].PostInitMigrations.MigrateGPUs = false
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
// create a docker client
dockerClient, err := migrator.dockerFactory.CreateClient(&environments[i], "", nil)
if err != nil {
log.Err(err).Msg("failure creating docker client for environment: " + environments[i].Name)
return
}
defer dockerClient.Close()
// get all containers
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{All: true})
if err != nil {
log.Err(err).Msg("failed to list containers")
return
}
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole endpoint
containersLoop:
for _, container := range containers {
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
if err != nil {
log.Err(err).Msg("failed to inspect container")
return
}
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
for _, deviceRequest := range deviceRequests {
if deviceRequest.Driver == "nvidia" {
environments[i].EnableGPUManagement = true
migrator.dataStore.Endpoint().UpdateEndpoint(environments[i].ID, &environments[i])
break containersLoop
}
}
}
}
}
}

View File

@@ -4,140 +4,122 @@ import (
"reflect"
"runtime"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/pkg/errors"
"github.com/Masterminds/semver"
"github.com/rs/zerolog/log"
)
type migration struct {
dbversion int
migrate func() error
}
func migrationError(err error, context string) error {
return errors.Wrap(err, "failed in "+context)
}
func newMigration(dbversion int, migrate func() error) migration {
return migration{
dbversion: dbversion,
migrate: migrate,
}
}
func dbTooOldError() error {
return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
}
func GetFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// set DB to updating status
err := m.versionService.StoreIsUpdating(true)
version, err := m.versionService.Version()
if err != nil {
return migrationError(err, "StoreIsUpdating")
return migrationError(err, "get version service")
}
migrations := []migration{
// Portainer < 1.21.0
newMigration(17, dbTooOldError),
// Portainer 1.21.0
newMigration(18, m.updateUsersToDBVersion18),
newMigration(18, m.updateEndpointsToDBVersion18),
newMigration(18, m.updateEndpointGroupsToDBVersion18),
newMigration(18, m.updateRegistriesToDBVersion18),
// 1.22.0
newMigration(19, m.updateSettingsToDBVersion19),
// 1.22.1
newMigration(20, m.updateUsersToDBVersion20),
newMigration(20, m.updateSettingsToDBVersion20),
newMigration(20, m.updateSchedulesToDBVersion20),
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
newMigration(22, m.updateResourceControlsToDBVersion22),
newMigration(22, m.updateUsersAndRolesToDBVersion22),
// Portainer 1.24.0
newMigration(23, m.updateTagsToDBVersion23),
newMigration(23, m.updateEndpointsAndEndpointGroupsToDBVersion23),
// Portainer 1.24.1
newMigration(24, m.updateSettingsToDB24),
// Portainer 2.0.0
newMigration(25, m.updateSettingsToDB25),
newMigration(25, m.updateStacksToDB24), // yes this looks odd. Don't be tempted to move it
// Portainer 2.1.0
newMigration(26, m.updateEndpointSettingsToDB25),
// Portainer 2.2.0
newMigration(27, m.updateStackResourceControlToDB27),
// Portainer 2.6.0
newMigration(30, m.migrateDBVersionToDB30),
// Portainer 2.9.0
newMigration(32, m.migrateDBVersionToDB32),
// Portainer 2.9.1, 2.9.2
newMigration(33, m.migrateDBVersionToDB33),
// Portainer 2.10
newMigration(34, m.migrateDBVersionToDB34),
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
newMigration(35, m.migrateDBVersionToDB35),
newMigration(36, m.migrateDBVersionToDB36),
// Portainer 2.13
newMigration(40, m.migrateDBVersionToDB40),
// Portainer 2.14
newMigration(50, m.migrateDBVersionToDB50),
// Portainer 2.15
newMigration(60, m.migrateDBVersionToDB60),
// Portainer 2.16
newMigration(70, m.migrateDBVersionToDB70),
schemaVersion, err := semver.NewVersion(version.SchemaVersion)
if err != nil {
return migrationError(err, "invalid db schema version")
}
var lastDbVersion int
for _, migration := range migrations {
if m.currentDBVersion < migration.dbversion {
newMigratorCount := 0
apiVersion := semver.MustParse(portainer.APIVersion)
if schemaVersion.Equal(apiVersion) {
// detect and run migrations when the versions are the same.
// e.g. development builds
latestMigrations := m.LatestMigrations()
if latestMigrations.Version.Equal(schemaVersion) &&
version.MigratorCount != len(latestMigrations.MigrationFuncs) {
err := runMigrations(latestMigrations.MigrationFuncs)
if err != nil {
return err
}
newMigratorCount = len(latestMigrations.MigrationFuncs)
}
} else {
// regular path when major/minor/patch versions differ
for _, migration := range m.migrations {
if schemaVersion.LessThan(migration.Version) {
// Print the next line only when the version changes
if migration.dbversion > lastDbVersion {
log.Info().Int("to_version", migration.dbversion).Msg("migrating DB")
log.Info().Msgf("migrating data to %s", migration.Version.String())
err := runMigrations(migration.MigrationFuncs)
if err != nil {
return err
}
}
err := migration.migrate()
if err != nil {
return migrationError(err, GetFunctionName(migration.migrate))
if apiVersion.Equal(migration.Version) {
newMigratorCount = len(migration.MigrationFuncs)
}
}
lastDbVersion = migration.dbversion
}
log.Info().Int("version", portainer.DBVersion).Msg("setting DB version")
err = m.Always()
if err != nil {
return migrationError(err, "Always migrations returned error")
}
err = m.versionService.StoreDBVersion(portainer.DBVersion)
version.SchemaVersion = portainer.APIVersion
version.MigratorCount = newMigratorCount
err = m.versionService.UpdateVersion(version)
if err != nil {
return migrationError(err, "StoreDBVersion")
}
log.Info().Int("version", portainer.DBVersion).Msg("updated DB version")
log.Info().Msgf("db migrated to %s", portainer.APIVersion)
// reset DB updating status
return m.versionService.StoreIsUpdating(false)
return nil
}
func runMigrations(migrationFuncs []func() error) error {
for _, migrationFunc := range migrationFuncs {
err := migrationFunc()
if err != nil {
return migrationError(err, GetFunctionName(migrationFunc))
}
}
return nil
}
func (m *Migrator) NeedsMigration() bool {
// we need to migrate if anything changes with the version in the DB vs what our software version is.
// If the version matches, then it's all down to the number of migration funcs we have for the current version
// i.e. the MigratorCount
// In this particular instance we should log a fatal error
if m.CurrentDBEdition() != portainer.PortainerCE {
log.Fatal().Msg("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
return false
}
if m.CurrentSemanticDBVersion().LessThan(semver.MustParse(portainer.APIVersion)) {
return true
}
// Check if we have any migrations for the current version
latestMigrations := m.LatestMigrations()
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
if m.currentDBVersion.MigratorCount != len(latestMigrations.MigrationFuncs) {
return true
}
} else {
// One remaining possibility if we get here. If our migrator count > 0 and we have no migration funcs
// for the current version (i.e. they were deleted during development). Then we we need to migrate.
// This is to reset the migrator count back to 0
if m.currentDBVersion.MigratorCount > 0 {
return true
}
}
return false
}

View File

@@ -3,7 +3,7 @@ package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/internal/stackutils"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log"
)

View File

@@ -12,7 +12,7 @@ func (m *Migrator) migrateDBVersionToDB34() error {
return MigrateStackEntryPoint(m.stackService)
}
// MigrateStackEntryPoint exported for testing (blah.)
// MigrateStackEntryPoint exported for testing
func MigrateStackEntryPoint(stackService dataservices.StackService) error {
stacks, err := stackService.Stacks()
if err != nil {

View File

@@ -1,12 +1,7 @@
package migrator
import "github.com/rs/zerolog/log"
func (m *Migrator) migrateDBVersionToDB35() error {
// These should have been migrated already, but due to an earlier bug and a bunch of duplicates,
// calling it again will now fix the issue as the function has been repaired.
log.Info().Msg("updating dockerhub registries")
return m.updateDockerhubToDB32()
}

View File

@@ -1,94 +0,0 @@
package migrator
const (
db35TestFile = "portainer-mig-35.db"
username = "portainer"
password = "password"
)
// TODO: this is exactly the kind of reaching into the internals of the store we should not do
// func setupDB35Test(t *testing.T) *Migrator {
// is := assert.New(t)
// dbConn, err := bolt.Open(path.Join(t.TempDir(), db35TestFile), 0600, &bolt.Options{Timeout: 1 * time.Second})
// is.NoError(err, "failed to init testing DB connection")
// // Create an old style dockerhub authenticated account
// dockerhubService, err := dockerhub.NewService(&database.DbConnection{DB: dbConn})
// is.NoError(err, "failed to init testing registry service")
// err = dockerhubService.UpdateDockerHub(&portainer.DockerHub{true, username, password})
// is.NoError(err, "failed to create dockerhub account")
// registryService, err := registry.NewService(&database.DbConnection{DB: dbConn})
// is.NoError(err, "failed to init testing registry service")
// endpointService, err := endpoint.NewService(&database.DbConnection{DB: dbConn})
// is.NoError(err, "failed to init endpoint service")
// m := &Migrator{
// db: dbConn,
// dockerhubService: dockerhubService,
// registryService: registryService,
// endpointService: endpointService,
// }
// return m
// }
// // TestUpdateDockerhubToDB32 tests a normal upgrade
// func TestUpdateDockerhubToDB32(t *testing.T) {
// is := assert.New(t)
// m := setupDB35Test(t)
// defer m.db.Close()
// defer os.Remove(db35TestFile)
// if err := m.updateDockerhubToDB32(); err != nil {
// t.Errorf("failed to update settings: %v", err)
// }
// // Verify we have a single registry were created
// registries, err := m.registryService.Registries()
// is.NoError(err, "failed to read registries from the RegistryService")
// is.Equal(len(registries), 1, "only one migrated registry expected")
// }
// // TestUpdateDockerhubToDB32_with_duplicate_migrations tests an upgrade where in earlier versions a broken migration
// // created a large number of duplicate "dockerhub migrated" registry entries.
// func TestUpdateDockerhubToDB32_with_duplicate_migrations(t *testing.T) {
// is := assert.New(t)
// m := setupDB35Test(t)
// defer m.db.Close()
// defer os.Remove(db35TestFile)
// // Create lots of duplicate entries...
// registry := &portainer.Registry{
// Type: portainer.DockerHubRegistry,
// Name: "Dockerhub (authenticated - migrated)",
// URL: "docker.io",
// Authentication: true,
// Username: "portainer",
// Password: "password",
// RegistryAccesses: portainer.RegistryAccesses{},
// }
// for i := 1; i < 150; i++ {
// err := m.registryService.CreateRegistry(registry)
// assert.NoError(t, err, "create registry failed")
// }
// // Verify they were created
// registries, err := m.registryService.Registries()
// is.NoError(err, "failed to read registries from the RegistryService")
// is.Condition(func() bool {
// return len(registries) > 1
// }, "expected multiple duplicate registry entries")
// // Now run the migrator
// if err := m.updateDockerhubToDB32(); err != nil {
// t.Errorf("failed to update settings: %v", err)
// }
// // Verify we have a single registry were created
// registries, err = m.registryService.Registries()
// is.NoError(err, "failed to read registries from the RegistryService")
// is.Equal(len(registries), 1, "only one migrated registry expected")
// }

View File

@@ -19,12 +19,13 @@ func (m *Migrator) addGpuInputFieldDB60() error {
}
for _, endpoint := range endpoints {
endpoint.Gpus = []portainer.Pair{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
if endpoint.Gpus == nil {
endpoint.Gpus = []portainer.Pair{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
}
return nil

View File

@@ -7,8 +7,8 @@ import (
)
func (m *Migrator) migrateDBVersionToDB70() error {
log.Info().Msg("- add IngressAvailabilityPerNamespace field")
if err := m.addIngressAvailabilityPerNamespaceFieldDB70(); err != nil {
log.Info().Msg("add IngressAvailabilityPerNamespace field")
if err := m.updateIngressFieldsForEnvDB70(); err != nil {
return err
}
@@ -51,7 +51,7 @@ func (m *Migrator) migrateDBVersionToDB70() error {
return nil
}
func (m *Migrator) addIngressAvailabilityPerNamespaceFieldDB70() error {
func (m *Migrator) updateIngressFieldsForEnvDB70() error {
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
@@ -59,6 +59,8 @@ func (m *Migrator) addIngressAvailabilityPerNamespaceFieldDB70() error {
for _, endpoint := range endpoints {
endpoint.Kubernetes.Configuration.IngressAvailabilityPerNamespace = true
endpoint.Kubernetes.Configuration.AllowNoneIngressClass = false
endpoint.PostInitMigrations.MigrateIngresses = true
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {

View File

@@ -0,0 +1,36 @@
package migrator
import (
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
func (m *Migrator) migrateDBVersionToDB71() error {
log.Info().Msg("removing orphaned snapshots")
snapshots, err := m.snapshotService.Snapshots()
if err != nil {
return err
}
for _, s := range snapshots {
_, err := m.endpointService.Endpoint(s.EndpointID)
if err == nil {
log.Debug().Int("endpoint_id", int(s.EndpointID)).Msg("keeping snapshot")
continue
} else if err != errors.ErrObjectNotFound {
log.Debug().Int("endpoint_id", int(s.EndpointID)).Err(err).Msg("database error")
return err
}
log.Debug().Int("endpoint_id", int(s.EndpointID)).Msg("removing snapshot")
err = m.snapshotService.DeleteSnapshot(s.EndpointID)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,102 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/rs/zerolog/log"
)
func (m *Migrator) migrateDBVersionToDB80() error {
if err := m.updateEdgeStackStatusForDB80(); err != nil {
return err
}
if err := m.updateExistingEndpointsToNotDetectMetricsAPIForDB80(); err != nil {
return err
}
if err := m.updateExistingEndpointsToNotDetectStorageAPIForDB80(); err != nil {
return err
}
return nil
}
func (m *Migrator) updateExistingEndpointsToNotDetectMetricsAPIForDB80() error {
log.Info().Msg("updating existing endpoints to not detect metrics API for existing endpoints (k8s)")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
if endpointutils.IsKubernetesEndpoint(&endpoint) {
endpoint.Kubernetes.Flags.IsServerMetricsDetected = true
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
}
return nil
}
func (m *Migrator) updateExistingEndpointsToNotDetectStorageAPIForDB80() error {
log.Info().Msg("updating existing endpoints to not detect metrics API for existing endpoints (k8s)")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
if endpointutils.IsKubernetesEndpoint(&endpoint) {
endpoint.Kubernetes.Flags.IsServerStorageDetected = true
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
}
return nil
}
func (m *Migrator) updateEdgeStackStatusForDB80() error {
log.Info().Msg("transfer type field to details field for edge stack status")
edgeStacks, err := m.edgeStackService.EdgeStacks()
if err != nil {
return err
}
for _, edgeStack := range edgeStacks {
for endpointId, status := range edgeStack.Status {
switch status.Type {
case portainer.EdgeStackStatusPending:
status.Details.Pending = true
case portainer.EdgeStackStatusOk:
status.Details.Ok = true
case portainer.EdgeStackStatusError:
status.Details.Error = true
case portainer.EdgeStackStatusAcknowledged:
status.Details.Acknowledged = true
case portainer.EdgeStackStatusRemove:
status.Details.Remove = true
case portainer.EdgeStackStatusRemoteUpdateSuccess:
status.Details.RemoteUpdateSuccess = true
}
edgeStack.Status[endpointId] = status
}
err = m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,92 @@
package migrator
import (
"github.com/rs/zerolog/log"
portainer "github.com/portainer/portainer/api"
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
)
func (m *Migrator) migrateDBVersionToDB90() error {
if err := m.updateUserThemeForDB90(); err != nil {
return err
}
if err := m.updateEnableGpuManagementFeatures(); err != nil {
return err
}
return m.updateEdgeStackStatusForDB90()
}
func (m *Migrator) updateEdgeStackStatusForDB90() error {
log.Info().Msg("clean up deleted endpoints from edge jobs")
edgeJobs, err := m.edgeJobService.EdgeJobs()
if err != nil {
return err
}
for _, edgeJob := range edgeJobs {
for endpointId := range edgeJob.Endpoints {
_, err := m.endpointService.Endpoint(endpointId)
if err == portainerDsErrors.ErrObjectNotFound {
delete(edgeJob.Endpoints, endpointId)
err = m.edgeJobService.UpdateEdgeJob(edgeJob.ID, &edgeJob)
if err != nil {
return err
}
}
}
}
return nil
}
func (m *Migrator) updateUserThemeForDB90() error {
log.Info().Msg("updating existing user theme settings")
users, err := m.userService.Users()
if err != nil {
return err
}
for i := range users {
user := &users[i]
if user.UserTheme != "" {
user.ThemeSettings.Color = user.UserTheme
}
if err := m.userService.UpdateUser(user.ID, user); err != nil {
return err
}
}
return nil
}
func (m *Migrator) updateEnableGpuManagementFeatures() error {
// get all environments
environments, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, environment := range environments {
if environment.Type == portainer.DockerEnvironment {
// set the PostInitMigrations.MigrateGPUs to true on this environment to run the migration only on the 2.18 upgrade
environment.PostInitMigrations.MigrateGPUs = true
// if there's one or more gpu, set the EnableGpuManagement setting to true
gpuList := environment.Gpus
if len(gpuList) > 0 {
environment.EnableGPUManagement = true
}
// update the environment
if err := m.endpointService.UpdateEndpoint(environment.ID, &environment); err != nil {
return err
}
}
}
return nil
}

View File

@@ -1,7 +1,16 @@
package migrator
import (
"errors"
"github.com/portainer/portainer/api/dataservices/edgejob"
"github.com/portainer/portainer/api/dataservices/edgestack"
"github.com/Masterminds/semver"
"github.com/rs/zerolog/log"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices/dockerhub"
"github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/endpointgroup"
@@ -25,7 +34,9 @@ import (
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
currentDBVersion *models.Version
migrations []Migrations
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
@@ -45,11 +56,13 @@ type (
fileService portainer.FileService
authorizationService *authorization.Service
dockerhubService *dockerhub.Service
edgeStackService *edgestack.Service
edgeJobService *edgejob.Service
}
// MigratorParameters represents the required parameters to create a new Migrator instance.
MigratorParameters struct {
DatabaseVersion int
CurrentDBVersion *models.Version
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
@@ -69,13 +82,15 @@ type (
FileService portainer.FileService
AuthorizationService *authorization.Service
DockerhubService *dockerhub.Service
EdgeStackService *edgestack.Service
EdgeJobService *edgejob.Service
}
)
// NewMigrator creates a new Migrator.
func NewMigrator(parameters *MigratorParameters) *Migrator {
return &Migrator{
currentDBVersion: parameters.DatabaseVersion,
migrator := &Migrator{
currentDBVersion: parameters.CurrentDBVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
@@ -95,10 +110,117 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
fileService: parameters.FileService,
authorizationService: parameters.AuthorizationService,
dockerhubService: parameters.DockerhubService,
edgeStackService: parameters.EdgeStackService,
edgeJobService: parameters.EdgeJobService,
}
migrator.initMigrations()
return migrator
}
// Version exposes version of database
func (migrator *Migrator) Version() int {
return migrator.currentDBVersion
func (m *Migrator) CurrentDBVersion() string {
return m.currentDBVersion.SchemaVersion
}
func (m *Migrator) CurrentDBEdition() portainer.SoftwareEdition {
return portainer.SoftwareEdition(m.currentDBVersion.Edition)
}
func (m *Migrator) CurrentSemanticDBVersion() *semver.Version {
v, err := semver.NewVersion(m.currentDBVersion.SchemaVersion)
if err != nil {
log.Fatal().Stack().Err(err).Msg("failed to parse current version")
}
return v
}
func (m *Migrator) addMigrations(v string, funcs ...func() error) {
m.migrations = append(m.migrations, Migrations{
Version: semver.MustParse(v),
MigrationFuncs: funcs,
})
}
func (m *Migrator) LatestMigrations() Migrations {
return m.migrations[len(m.migrations)-1]
}
// !NOTE: Migration funtions should ideally be idempotent.
// ! Which simply means the function can run over the same data many times but only transform it once.
// ! In practice this really just means an extra check or two to ensure we're not destroying valid data.
// ! This is not a hard rule though. Understand the limitations. A migration function may only run over
// ! the data more than once if a new migration function is added and the version of your database schema is
// ! the same. e.g. two developers working on the same version add two different functions for different things.
// ! This increases the migration funcs count and so they all run again.
type Migrations struct {
Version *semver.Version
MigrationFuncs MigrationFuncs
}
type MigrationFuncs []func() error
func (m *Migrator) initMigrations() {
// !IMPORTANT: Do not be tempted to alter the order of these migrations.
// ! Even though one of them looks out of order. Caused by history related
// ! to maintaining two versions and releasing at different times
m.addMigrations("1.0.0", dbTooOldError) // default version found after migration
m.addMigrations("1.21",
m.updateUsersToDBVersion18,
m.updateEndpointsToDBVersion18,
m.updateEndpointGroupsToDBVersion18,
m.updateRegistriesToDBVersion18)
m.addMigrations("1.22", m.updateSettingsToDBVersion19)
m.addMigrations("1.22.1",
m.updateUsersToDBVersion20,
m.updateSettingsToDBVersion20,
m.updateSchedulesToDBVersion20)
m.addMigrations("1.23",
m.updateResourceControlsToDBVersion22,
m.updateUsersAndRolesToDBVersion22)
m.addMigrations("1.24",
m.updateTagsToDBVersion23,
m.updateEndpointsAndEndpointGroupsToDBVersion23)
m.addMigrations("1.24.1", m.updateSettingsToDB24)
m.addMigrations("2.0",
m.updateSettingsToDB25,
m.updateStacksToDB24)
m.addMigrations("2.1", m.updateEndpointSettingsToDB25)
m.addMigrations("2.2", m.updateStackResourceControlToDB27)
m.addMigrations("2.6", m.migrateDBVersionToDB30)
m.addMigrations("2.9", m.migrateDBVersionToDB32)
m.addMigrations("2.9.2", m.migrateDBVersionToDB33)
m.addMigrations("2.10.0", m.migrateDBVersionToDB34)
m.addMigrations("2.9.3", m.migrateDBVersionToDB35)
m.addMigrations("2.12", m.migrateDBVersionToDB36)
m.addMigrations("2.13", m.migrateDBVersionToDB40)
m.addMigrations("2.14", m.migrateDBVersionToDB50)
m.addMigrations("2.15", m.migrateDBVersionToDB60)
m.addMigrations("2.16", m.migrateDBVersionToDB70)
m.addMigrations("2.16.1", m.migrateDBVersionToDB71)
m.addMigrations("2.17", m.migrateDBVersionToDB80)
m.addMigrations("2.18", m.migrateDBVersionToDB90)
// Add new migrations below...
// One function per migration, each versions migration funcs in the same file.
}
// Always is always run at the end of migrations
func (m *Migrator) Always() error {
// currently nothing to be done in CE... yet
return nil
}
func dbTooOldError() error {
return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support")
}

View File

@@ -3,10 +3,10 @@ package datastore
import (
"encoding/json"
"fmt"
"io/ioutil"
"strconv"
"os"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/dataservices/apikeyrepository"
"github.com/portainer/portainer/api/dataservices/customtemplate"
@@ -14,7 +14,6 @@ import (
"github.com/portainer/portainer/api/dataservices/edgegroup"
"github.com/portainer/portainer/api/dataservices/edgejob"
"github.com/portainer/portainer/api/dataservices/edgestack"
"github.com/portainer/portainer/api/dataservices/edgeupdateschedule"
"github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
@@ -50,7 +49,6 @@ type Store struct {
DockerHubService *dockerhub.Service
EdgeGroupService *edgegroup.Service
EdgeJobService *edgejob.Service
EdgeUpdateScheduleService *edgeupdateschedule.Service
EdgeStackService *edgestack.Service
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
@@ -95,17 +93,18 @@ func (store *Store) initServices() error {
}
store.DockerHubService = dockerhubService
edgeUpdateScheduleService, err := edgeupdateschedule.NewService(store.connection)
endpointRelationService, err := endpointrelation.NewService(store.connection)
if err != nil {
return err
}
store.EdgeUpdateScheduleService = edgeUpdateScheduleService
store.EndpointRelationService = endpointRelationService
edgeStackService, err := edgestack.NewService(store.connection)
edgeStackService, err := edgestack.NewService(store.connection, endpointRelationService.InvalidateEdgeCacheForEdgeStack)
if err != nil {
return err
}
store.EdgeStackService = edgeStackService
endpointRelationService.RegisterUpdateStackFunction(edgeStackService.UpdateEdgeStackFunc, edgeStackService.UpdateEdgeStackFuncTx)
edgeGroupService, err := edgegroup.NewService(store.connection)
if err != nil {
@@ -131,12 +130,6 @@ func (store *Store) initServices() error {
}
store.EndpointService = endpointService
endpointRelationService, err := endpointrelation.NewService(store.connection)
if err != nil {
return err
}
store.EndpointRelationService = endpointRelationService
extensionService, err := extension.NewService(store.connection)
if err != nil {
return err
@@ -263,11 +256,6 @@ func (store *Store) EdgeJob() dataservices.EdgeJobService {
return store.EdgeJobService
}
// EdgeUpdateSchedule gives access to the EdgeUpdateSchedule data management layer
func (store *Store) EdgeUpdateSchedule() dataservices.EdgeUpdateScheduleService {
return store.EdgeUpdateScheduleService
}
// EdgeStack gives access to the EdgeStack data management layer
func (store *Store) EdgeStack() dataservices.EdgeStackService {
return store.EdgeStackService
@@ -395,7 +383,7 @@ type storeExport struct {
Team []portainer.Team `json:"teams,omitempty"`
TunnelServer portainer.TunnelServerInfo `json:"tunnel_server,omitempty"`
User []portainer.User `json:"users,omitempty"`
Version map[string]string `json:"version,omitempty"`
Version models.Version `json:"version,omitempty"`
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
@@ -588,14 +576,12 @@ func (store *Store) Export(filename string) (err error) {
backup.Webhook = webhooks
}
v, err := store.Version().DBVersion()
if err != nil && !store.IsErrObjectNotFound(err) {
log.Error().Err(err).Msg("exporting DB version")
}
instance, _ := store.Version().InstanceID()
backup.Version = map[string]string{
"DB_VERSION": strconv.Itoa(v),
"INSTANCE_ID": instance,
if version, err := store.Version().Version(); err != nil {
if !store.IsErrObjectNotFound(err) {
log.Error().Err(err).Msg("exporting Version")
}
} else {
backup.Version = *version
}
backup.Metadata, err = store.connection.BackupMetadata()
@@ -607,13 +593,13 @@ func (store *Store) Export(filename string) (err error) {
if err != nil {
return err
}
return ioutil.WriteFile(filename, b, 0600)
return os.WriteFile(filename, b, 0600)
}
func (store *Store) Import(filename string) (err error) {
backup := storeExport{}
s, err := ioutil.ReadFile(filename)
s, err := os.ReadFile(filename)
if err != nil {
return err
}
@@ -622,19 +608,7 @@ func (store *Store) Import(filename string) (err error) {
return err
}
// TODO: yup, this is bad, and should be in a version struct...
if dbversion, ok := backup.Version["DB_VERSION"]; ok {
if v, err := strconv.Atoi(dbversion); err == nil {
if err := store.Version().StoreDBVersion(v); err != nil {
log.Error().Err(err).Msg("DB_VERSION import issue")
}
}
}
if instanceID, ok := backup.Version["INSTANCE_ID"]; ok {
if err := store.Version().StoreInstanceID(instanceID); err != nil {
log.Error().Err(err).Msg("INSTANCE_ID import issue")
}
}
store.Version().UpdateVersion(&backup.Version)
for _, v := range backup.CustomTemplate {
store.CustomTemplate().UpdateCustomTemplate(v.ID, &v)

View File

@@ -0,0 +1,71 @@
package datastore
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
type StoreTx struct {
store *Store
tx portainer.Transaction
}
func (tx *StoreTx) IsErrObjectNotFound(err error) bool {
return tx.store.IsErrObjectNotFound(err)
}
func (tx *StoreTx) CustomTemplate() dataservices.CustomTemplateService { return nil }
func (tx *StoreTx) EdgeGroup() dataservices.EdgeGroupService {
return tx.store.EdgeGroupService.Tx(tx.tx)
}
func (tx *StoreTx) EdgeJob() dataservices.EdgeJobService {
return tx.store.EdgeJobService.Tx(tx.tx)
}
func (tx *StoreTx) EdgeStack() dataservices.EdgeStackService {
return tx.store.EdgeStackService.Tx(tx.tx)
}
func (tx *StoreTx) Endpoint() dataservices.EndpointService {
return tx.store.EndpointService.Tx(tx.tx)
}
func (tx *StoreTx) EndpointGroup() dataservices.EndpointGroupService {
return tx.store.EndpointGroupService.Tx(tx.tx)
}
func (tx *StoreTx) EndpointRelation() dataservices.EndpointRelationService {
return tx.store.EndpointRelationService.Tx(tx.tx)
}
func (tx *StoreTx) FDOProfile() dataservices.FDOProfileService { return nil }
func (tx *StoreTx) HelmUserRepository() dataservices.HelmUserRepositoryService { return nil }
func (tx *StoreTx) Registry() dataservices.RegistryService {
return nil
}
func (tx *StoreTx) ResourceControl() dataservices.ResourceControlService { return nil }
func (tx *StoreTx) Role() dataservices.RoleService { return nil }
func (tx *StoreTx) APIKeyRepository() dataservices.APIKeyRepository { return nil }
func (tx *StoreTx) Settings() dataservices.SettingsService { return nil }
func (tx *StoreTx) Snapshot() dataservices.SnapshotService {
return tx.store.SnapshotService.Tx(tx.tx)
}
func (tx *StoreTx) SSLSettings() dataservices.SSLSettingsService { return nil }
func (tx *StoreTx) Stack() dataservices.StackService { return nil }
func (tx *StoreTx) Tag() dataservices.TagService {
return tx.store.TagService.Tx(tx.tx)
}
func (tx *StoreTx) TeamMembership() dataservices.TeamMembershipService { return nil }
func (tx *StoreTx) Team() dataservices.TeamService { return nil }
func (tx *StoreTx) TunnelServer() dataservices.TunnelServerService { return nil }
func (tx *StoreTx) User() dataservices.UserService { return nil }
func (tx *StoreTx) Version() dataservices.VersionService { return nil }
func (tx *StoreTx) Webhook() dataservices.WebhookService { return nil }

View File

@@ -37,6 +37,7 @@
"EdgeKey": "",
"Extensions": [],
"GroupId": 1,
"Heartbeat": false,
"Id": 1,
"Name": "local",
"PublicURL": "",

View File

@@ -46,12 +46,15 @@
},
"EdgeCheckinInterval": 0,
"EdgeKey": "",
"EnableGPUManagement": false,
"Gpus": [],
"GroupId": 1,
"Heartbeat": false,
"Id": 1,
"IsEdgeDevice": false,
"Kubernetes": {
"Configuration": {
"AllowNoneIngressClass": false,
"EnableResourceOverCommit": false,
"IngressAvailabilityPerNamespace": true,
"IngressClasses": null,
@@ -61,10 +64,19 @@
"UseLoadBalancer": false,
"UseServerMetrics": false
},
"Flags": {
"IsServerIngressClassDetected": false,
"IsServerMetricsDetected": false,
"IsServerStorageDetected": false
},
"Snapshots": []
},
"LastCheckInDate": 0,
"Name": "local",
"PostInitMigrations": {
"MigrateGPUs": true,
"MigrateIngresses": true
},
"PublicURL": "",
"QueryDate": 0,
"SecuritySettings": {
@@ -636,6 +648,7 @@
"Scopes": "",
"UserIdentifier": ""
},
"ShowKomposeBuildOption": false,
"SnapshotInterval": "5m",
"TemplatesURL": "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json",
"TrustOnFirstConnect": false,
@@ -893,6 +906,9 @@
"PortainerUserRevokeToken": true
},
"Role": 1,
"ThemeSettings": {
"color": ""
},
"TokenIssueAt": 0,
"UserTheme": "",
"Username": "admin"
@@ -920,14 +936,15 @@
"PortainerUserRevokeToken": true
},
"Role": 1,
"ThemeSettings": {
"color": ""
},
"TokenIssueAt": 0,
"UserTheme": "",
"Username": "prabhat"
}
],
"version": {
"DB_UPDATING": "false",
"DB_VERSION": "80",
"INSTANCE_ID": "null"
"VERSION": "{\"SchemaVersion\":\"2.19.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}

View File

@@ -5,34 +5,29 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/filesystem"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
var errTempDir = errors.New("can't create a temp dir")
func (store *Store) GetConnection() portainer.Connection {
return store.connection
}
func MustNewTestStore(t *testing.T, init, secure bool) (bool, *Store, func()) {
func MustNewTestStore(t testing.TB, init, secure bool) (bool, *Store, func()) {
newStore, store, teardown, err := NewTestStore(t, init, secure)
if err != nil {
if !errors.Is(err, errTempDir) {
teardown()
}
log.Fatal().Err(err).Msg("")
}
return newStore, store, teardown
}
func NewTestStore(t *testing.T, init, secure bool) (bool, *Store, func(), error) {
func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner.
storePath := t.TempDir()
fileService, err := filesystem.NewService(storePath, "")
if err != nil {
return false, nil, nil, err
@@ -54,8 +49,6 @@ func NewTestStore(t *testing.T, init, secure bool) (bool, *Store, func(), error)
return newStore, nil, nil, err
}
log.Debug().Msg("opened")
if init {
err = store.Init()
if err != nil {
@@ -63,26 +56,24 @@ func NewTestStore(t *testing.T, init, secure bool) (bool, *Store, func(), error)
}
}
log.Debug().Msg("initialised")
if newStore {
// from MigrateData
store.VersionService.StoreDBVersion(portainer.DBVersion)
v := models.Version{
SchemaVersion: portainer.APIVersion,
Edition: int(portainer.PortainerCE),
}
err = store.VersionService.UpdateVersion(&v)
if err != nil {
return newStore, nil, nil, err
}
}
teardown := func() {
teardown(store)
err := store.Close()
if err != nil {
log.Fatal().Err(err).Msg("")
}
}
return newStore, store, teardown, nil
}
func teardown(store *Store) {
err := store.Close()
if err != nil {
log.Fatal().Err(err).Msg("")
}
}

View File

@@ -38,17 +38,19 @@ func NewClientFactory(signatureService portainer.DigitalSignatureService, revers
// with an agent enabled environment(endpoint) to target a specific node in an agent cluster.
// The underlying http client timeout may be specified, a default value is used otherwise.
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeName string, timeout *time.Duration) (*client.Client, error) {
if endpoint.Type == portainer.AzureEnvironment {
switch endpoint.Type {
case portainer.AzureEnvironment:
return nil, errUnsupportedEnvironmentType
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
case portainer.AgentOnDockerEnvironment:
return createAgentClient(endpoint, factory.signatureService, nodeName, timeout)
} else if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
case portainer.EdgeAgentOnDockerEnvironment:
return createEdgeClient(endpoint, factory.signatureService, factory.reverseTunnelService, nodeName, timeout)
}
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
return createLocalClient(endpoint)
}
return createTCPClient(endpoint, timeout)
}
@@ -59,6 +61,19 @@ func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
)
}
func CreateClientFromEnv() (*client.Client, error) {
return client.NewClientWithOpts(
client.FromEnv,
client.WithVersion(dockerClientVersion),
)
}
func CreateSimpleClient() (*client.Client, error) {
return client.NewClientWithOpts(
client.WithVersion(dockerClientVersion),
)
}
func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*client.Client, error) {
httpCli, err := httpClient(endpoint, timeout)
if err != nil {

View File

@@ -159,7 +159,7 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
gpuUseSet := make(map[string]struct{})
gpuUseAll := false
for _, container := range containers {
if container.State == "exited" {
if container.State == "exited" || container.State == "stopped" {
stoppedContainers++
} else if container.State == "running" {
runningContainers++
@@ -167,22 +167,29 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
// snapshot GPUs
response, err := cli.ContainerInspect(context.Background(), container.ID)
if err != nil {
return err
}
var gpuOptions *_container.DeviceRequest = nil
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
gpuOptions = &deviceRequest
// Inspect a container will fail when the container runs on a different
// Swarm node, so it is better to log the error instead of return error
// when the Swarm mode is enabled
if !snapshot.Swarm {
return err
} else {
log.Info().Str("container", container.ID).Err(err).Msg("unable to inspect container in other Swarm nodes")
}
}
if gpuOptions != nil {
if gpuOptions.Count == -1 {
gpuUseAll = true
} else {
var gpuOptions *_container.DeviceRequest = nil
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
gpuOptions = &deviceRequest
}
}
for _, id := range gpuOptions.DeviceIDs {
gpuUseSet[id] = struct{}{}
if gpuOptions != nil {
if gpuOptions.Count == -1 {
gpuUseAll = true
}
for _, id := range gpuOptions.DeviceIDs {
gpuUseSet[id] = struct{}{}
}
}
}
}
@@ -213,7 +220,9 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
snapshot.HealthyContainerCount = healthyContainers
snapshot.UnhealthyContainerCount = unhealthyContainers
snapshot.StackCount += len(stacks)
snapshot.SnapshotRaw.Containers = containers
for _, container := range containers {
snapshot.SnapshotRaw.Containers = append(snapshot.SnapshotRaw.Containers, portainer.DockerContainerSnapshot{Container: container})
}
return nil
}

View File

@@ -1,102 +0,0 @@
package edgetypes
import portainer "github.com/portainer/portainer/api"
const (
// PortainerAgentUpdateScheduleIDHeader represents the name of the header containing the update schedule id
PortainerAgentUpdateScheduleIDHeader = "X-Portainer-Update-Schedule-ID"
// PortainerAgentUpdateStatusHeader is the name of the header that will have the update status
PortainerAgentUpdateStatusHeader = "X-Portainer-Update-Status"
// PortainerAgentUpdateErrorHeader is the name of the header that will have the update error
PortainerAgentUpdateErrorHeader = "X-Portainer-Update-Error"
)
type (
// UpdateScheduleID represents an Edge schedule identifier
UpdateScheduleID int
// UpdateSchedule represents a schedule for update/rollback of edge devices
UpdateSchedule struct {
// EdgeUpdateSchedule Identifier
ID UpdateScheduleID `json:"id" example:"1"`
// Name of the schedule
Name string `json:"name" example:"Update Schedule"`
// Type of the schedule
Time int64 `json:"time" example:"1564897200"`
// EdgeGroups to be updated
GroupIDs []portainer.EdgeGroupID `json:"groupIds" example:"1"`
// Type of the update (1 - update, 2 - rollback)
Type UpdateScheduleType `json:"type" example:"1" enums:"1,2"`
// Status of the schedule, grouped by environment id
Status map[portainer.EndpointID]UpdateScheduleStatus `json:"status"`
// Created timestamp
Created int64 `json:"created" example:"1564897200"`
// Created by user id
CreatedBy portainer.UserID `json:"createdBy" example:"1"`
}
// UpdateScheduleType represents type of an Edge update schedule
UpdateScheduleType int
// UpdateScheduleStatus represents status of an Edge update schedule
UpdateScheduleStatus struct {
// Status of the schedule (0 - pending, 1 - failed, 2 - success)
Status UpdateScheduleStatusType `json:"status" example:"1" enums:"1,2,3"`
// Error message if status is failed
Error string `json:"error" example:""`
// Target version of the edge agent
TargetVersion string `json:"targetVersion" example:"1"`
// Current version of the edge agent
CurrentVersion string `json:"currentVersion" example:"1"`
}
// UpdateScheduleStatusType represents status type of an Edge update schedule
UpdateScheduleStatusType int
VersionUpdateRequest struct {
// Target version
Version string
// Scheduled time
ScheduledTime int64
// If need to update
Active bool
// Update schedule ID
ScheduleID UpdateScheduleID
}
// VersionUpdateStatus represents the status of an agent version update
VersionUpdateStatus struct {
Status UpdateScheduleStatusType
ScheduleID UpdateScheduleID
Error string
}
// EndpointUpdateScheduleRelation represents the relation between an environment(endpoint) and an update schedule
EndpointUpdateScheduleRelation struct {
EnvironmentID portainer.EndpointID `json:"environmentId"`
ScheduleID UpdateScheduleID `json:"scheduleId"`
TargetVersion string `json:"targetVersion"`
Status UpdateScheduleStatusType `json:"status"`
Error string `json:"error"`
Type UpdateScheduleType `json:"type"`
ScheduledTime int64 `json:"scheduledTime"`
}
)
const (
_ UpdateScheduleType = iota
// UpdateScheduleUpdate represents an edge device scheduled for an update
UpdateScheduleUpdate
// UpdateScheduleRollback represents an edge device scheduled for a rollback
UpdateScheduleRollback
)
const (
// UpdateScheduleStatusPending represents a pending edge update schedule
UpdateScheduleStatusPending UpdateScheduleStatusType = iota
// UpdateScheduleStatusError represents a failed edge update schedule
UpdateScheduleStatusError
// UpdateScheduleStatusSuccess represents a successful edge update schedule
UpdateScheduleStatusSuccess
)

View File

@@ -9,11 +9,10 @@ import (
"strings"
libstack "github.com/portainer/docker-compose-wrapper"
"github.com/portainer/docker-compose-wrapper/compose"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/internal/stackutils"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/pkg/errors"
)
@@ -25,11 +24,7 @@ type ComposeStackManager struct {
}
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil
func NewComposeStackManager(binaryPath string, configPath string, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
deployer, err := compose.NewComposeDeployer(binaryPath, configPath)
if err != nil {
return nil, err
}
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager) (*ComposeStackManager, error) {
return &ComposeStackManager{
deployer: deployer,
@@ -43,7 +38,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRereate bool) error {
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRecreate bool) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy")
@@ -53,17 +48,25 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
defer proxy.Close()
}
envFile, err := createEnvFile(stack)
envFilePath, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile, forceRereate)
filePaths := stackutils.GetStackFilePaths(stack, true)
err = manager.deployer.Deploy(ctx, filePaths, libstack.DeployOptions{
Options: libstack.Options{
WorkingDir: stack.ProjectPath,
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
},
ForceRecreate: forceRecreate,
})
return errors.Wrap(err, "failed to deploy a stack")
}
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
// Down stops and removes containers, networks, images, and volumes
func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
@@ -73,14 +76,17 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
defer proxy.Close()
}
envFile, err := createEnvFile(stack)
envFilePath, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{
WorkingDir: stack.ProjectPath,
EnvFilePath: envFilePath,
Host: url,
})
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
return errors.Wrap(err, "failed to remove a stack")
}
@@ -95,13 +101,18 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
defer proxy.Close()
}
envFile, err := createEnvFile(stack)
envFilePath, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Pull(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
filePaths := stackutils.GetStackFilePaths(stack, true)
err = manager.deployer.Pull(ctx, filePaths, libstack.Options{
WorkingDir: stack.ProjectPath,
EnvFilePath: envFilePath,
Host: url,
ProjectName: stack.Name,
})
return errors.Wrap(err, "failed to pull images of the stack")
}
@@ -126,7 +137,7 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
// createEnvFile creates a file that would hold both "in-place" and default environment variables.
// It will return the name of the file if the stack has "in-place" env vars, otherwise empty string.
func createEnvFile(stack *portainer.Stack) (string, error) {
if stack.Env == nil || len(stack.Env) == 0 {
if len(stack.Env) == 0 {
return "", nil
}

View File

@@ -9,6 +9,7 @@ import (
"strings"
"testing"
"github.com/portainer/docker-compose-wrapper/compose"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/testhelpers"
@@ -47,7 +48,12 @@ func Test_UpAndDown(t *testing.T) {
stack, endpoint := setup(t)
w, err := NewComposeStackManager("", "", nil)
deployer, err := compose.NewComposeDeployer("", "")
if err != nil {
t.Fatal(err)
}
w, err := NewComposeStackManager(deployer, nil)
if err != nil {
t.Fatalf("Failed creating manager: %s", err)
}

View File

@@ -1,7 +1,7 @@
package exec
import (
"io/ioutil"
"io"
"os"
"path"
"testing"
@@ -55,7 +55,7 @@ func Test_createEnvFile(t *testing.T) {
assert.Equal(t, "stack.env", result)
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
content, _ := io.ReadAll(f)
assert.Equal(t, tt.expected, string(content))
} else {
@@ -80,7 +80,7 @@ func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
assert.NoError(t, err)
assert.FileExists(t, path.Join(dir, "stack.env"))
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
content, _ := io.ReadAll(f)
assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content)
}

View File

@@ -49,7 +49,7 @@ func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *
return "", err
}
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(endpoint.ID)
tokenManager, err := kubernetes.NewTokenManager(kubeCLI, deployer.dataStore, tokenCache, setLocalAdminToken)
if err != nil {

View File

@@ -14,7 +14,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/internal/stackutils"
"github.com/portainer/portainer/api/stacks/stackutils"
)
// SwarmStackManager represents a service for managing stacks.
@@ -59,6 +59,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
if err != nil {
return err
}
for _, registry := range registries {
if registry.Authentication {
err = registryutils.EnsureRegTokenValid(manager.dataStore, &registry)
@@ -75,6 +76,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
}
}
return nil
}
@@ -84,13 +86,15 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
if err != nil {
return err
}
args = append(args, "logout")
return runCommandAndCaptureStdErr(command, args, nil, "")
}
// Deploy executes the docker stack deploy command.
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pullImage bool, endpoint *portainer.Endpoint) error {
filePaths := stackutils.GetStackFilePaths(stack)
filePaths := stackutils.GetStackFilePaths(stack, true)
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
if err != nil {
return err
@@ -101,6 +105,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pul
} else {
args = append(args, "stack", "deploy", "--with-registry-auth")
}
if !pullImage {
args = append(args, "--resolve-image=never")
}
@@ -112,6 +117,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pul
for _, envvar := range stack.Env {
env = append(env, envvar.Name+"="+envvar.Value)
}
return runCommandAndCaptureStdErr(command, args, env, stack.ProjectPath)
}
@@ -121,7 +127,9 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta
if err != nil {
return err
}
args = append(args, "stack", "rm", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil, "")
}
@@ -198,17 +206,13 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
if config["HttpHeaders"] == nil {
config["HttpHeaders"] = make(map[string]interface{})
}
headersObject := config["HttpHeaders"].(map[string]interface{})
headersObject["X-PortainerAgent-ManagerOperation"] = "1"
headersObject["X-PortainerAgent-Signature"] = signature
headersObject["X-PortainerAgent-PublicKey"] = manager.signatureService.EncodedPublicKey()
err = manager.fileService.WriteJSONToFile(configFilePath, config)
if err != nil {
return err
}
return nil
return manager.fileService.WriteJSONToFile(configFilePath, config)
}
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
@@ -235,5 +239,6 @@ func configureFilePaths(args []string, filePaths []string) []string {
for _, path := range filePaths {
args = append(args, "--compose-file", path)
}
return args
}

View File

@@ -1,7 +1,6 @@
package filesystem
import (
"io/ioutil"
"os"
"path"
"path/filepath"
@@ -19,12 +18,12 @@ func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) {
func Test_copyFile_shouldMakeAbackup(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "origin"), content, 0600)
os.WriteFile(path.Join(tmpdir, "origin"), content, 0600)
err := copyFile(path.Join(tmpdir, "origin"), path.Join(tmpdir, "copy"))
assert.NoError(t, err)
copyContent, _ := ioutil.ReadFile(path.Join(tmpdir, "copy"))
copyContent, _ := os.ReadFile(path.Join(tmpdir, "copy"))
assert.Equal(t, content, copyContent)
}
@@ -59,13 +58,13 @@ func Test_CopyPath_shouldSkipWhenNotExist(t *testing.T) {
func Test_CopyPath_shouldCopyFile(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "file"), content, 0600)
os.WriteFile(path.Join(tmpdir, "file"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "backup"), 0700)
err := CopyPath(path.Join(tmpdir, "file"), path.Join(tmpdir, "backup"))
assert.NoError(t, err)
copyContent, err := ioutil.ReadFile(path.Join(tmpdir, "backup", "file"))
copyContent, err := os.ReadFile(path.Join(tmpdir, "backup", "file"))
assert.NoError(t, err)
assert.Equal(t, content, copyContent)
}

View File

@@ -62,6 +62,10 @@ const (
SSLKeyFilename = "key.pem"
// SSLCACertFilename represents the CA ssl certificate file name for mTLS
SSLCACertFilename = "ca-cert.pem"
MTLSCertFilename = "mtls-cert.pem"
MTLSCACertFilename = "mtls-ca-cert.pem"
MTLSKeyFilename = "mtls-key.pem"
)
// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type
@@ -163,7 +167,7 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
}
if !exists {
return errors.New(fmt.Sprintf("File (%s) doesn't exist", fromFilePath))
return fmt.Errorf("File (%s) doesn't exist", fromFilePath)
}
finput, err := os.Open(fromFilePath)
@@ -489,14 +493,7 @@ func (service *Service) createDirectoryInStore(name string) error {
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
path := service.wrapFileStore(filePath)
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
return err
return CreateFile(path, r)
}
// createBackupFileInStore makes a copy in the file store.
@@ -670,6 +667,14 @@ func (service *Service) GetDefaultSSLCertsPath() (string, string) {
return service.wrapFileStore(certPath), service.wrapFileStore(keyPath)
}
func defaultMTLSCertPathUnderFileStore() (string, string, string) {
certPath := JoinPaths(SSLCertPath, MTLSCertFilename)
caCertPath := JoinPaths(SSLCertPath, MTLSCACertFilename)
keyPath := JoinPaths(SSLCertPath, MTLSKeyFilename)
return certPath, caCertPath, keyPath
}
// StoreSSLCertPair stores a ssl certificate pair
func (service *Service) StoreSSLCertPair(cert, key []byte) (string, string, error) {
certPath, keyPath := defaultCertPathUnderFileStore()
@@ -762,3 +767,39 @@ func (service *Service) StoreFDOProfileFileFromBytes(fdoProfileIdentifier string
return service.wrapFileStore(filePath), nil
}
func CreateFile(path string, r io.Reader) error {
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
return err
}
func (service *Service) StoreMTLSCertificates(cert, caCert, key []byte) (string, string, string, error) {
certPath, caCertPath, keyPath := defaultMTLSCertPathUnderFileStore()
r := bytes.NewReader(cert)
err := service.createFileInStore(certPath, r)
if err != nil {
return "", "", "", err
}
r = bytes.NewReader(caCert)
err = service.createFileInStore(caCertPath, r)
if err != nil {
return "", "", "", err
}
r = bytes.NewReader(key)
err = service.createFileInStore(keyPath, r)
if err != nil {
return "", "", "", err
}
return service.wrapFileStore(certPath), service.wrapFileStore(caCertPath), service.wrapFileStore(keyPath), nil
}

View File

@@ -1,7 +1,7 @@
package filesystem
import (
"io/ioutil"
"os"
"path"
"testing"
@@ -16,7 +16,7 @@ func Test_WriteFile_CanStoreContentInANewFile(t *testing.T) {
err := WriteToFile(tmpFilePath, content)
assert.NoError(t, err)
fileContent, _ := ioutil.ReadFile(tmpFilePath)
fileContent, _ := os.ReadFile(tmpFilePath)
assert.Equal(t, content, fileContent)
}
@@ -31,7 +31,7 @@ func Test_WriteFile_CanOverwriteExistingFile(t *testing.T) {
err = WriteToFile(tmpFilePath, content)
assert.NoError(t, err)
fileContent, _ := ioutil.ReadFile(tmpFilePath)
fileContent, _ := os.ReadFile(tmpFilePath)
assert.Equal(t, content, fileContent)
}
@@ -43,6 +43,6 @@ func Test_WriteFile_CanWriteANestedPath(t *testing.T) {
err := WriteToFile(tmpFilePath, content)
assert.NoError(t, err)
fileContent, _ := ioutil.ReadFile(tmpFilePath)
fileContent, _ := os.ReadFile(tmpFilePath)
assert.Equal(t, content, fileContent)
}

View File

@@ -2,21 +2,20 @@ package git
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/go-git/go-git/v5/plumbing/transport/client"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/pkg/errors"
)
const (
@@ -50,28 +49,30 @@ type azureItem struct {
}
type azureClient struct {
client *http.Client
baseUrl string
}
func NewAzureClient() *azureClient {
httpsCli := newHttpClientForAzure()
return &azureClient{
client: httpsCli,
baseUrl: "https://dev.azure.com",
}
}
func newHttpClientForAzure() *http.Client {
func newHttpClientForAzure(insecureSkipVerify bool) *http.Client {
tlsConfig := crypto.CreateTLSConfiguration()
if insecureSkipVerify {
tlsConfig.InsecureSkipVerify = true
}
httpsCli := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSClientConfig: tlsConfig,
Proxy: http.ProxyFromEnvironment,
},
Timeout: 300 * time.Second,
}
client.InstallProtocol("https", githttp.NewClient(httpsCli))
return httpsCli
}
@@ -95,14 +96,17 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
if err != nil {
return "", errors.WithMessage(err, "failed to parse url")
}
downloadUrl, err := a.buildDownloadUrl(config, opt.referenceName)
if err != nil {
return "", errors.WithMessage(err, "failed to build download url")
}
zipFile, err := ioutil.TempFile("", "azure-git-repo-*.zip")
zipFile, err := os.CreateTemp("", "azure-git-repo-*.zip")
if err != nil {
return "", errors.WithMessage(err, "failed to create temp file")
}
defer zipFile.Close()
req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil)
@@ -116,10 +120,14 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
return "", errors.WithMessage(err, "failed to create a new HTTP request")
}
res, err := a.client.Do(req)
client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
res, err := client.Do(req)
if err != nil {
return "", errors.WithMessage(err, "failed to make an HTTP request")
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
@@ -130,6 +138,7 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
if err != nil {
return "", errors.WithMessage(err, "failed to save HTTP response to a file")
}
return zipFile.Name(), nil
}
@@ -138,6 +147,7 @@ func (a *azureClient) latestCommitID(ctx context.Context, opt fetchOption) (stri
if err != nil {
return "", err
}
return rootItem.CommitId, nil
}
@@ -163,7 +173,10 @@ func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureI
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
}
resp, err := a.client.Do(req)
client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithMessage(err, "failed to make an HTTP request")
}
@@ -184,6 +197,7 @@ func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureI
if len(items.Value) == 0 || items.Value[0].CommitId == "" {
return nil, errors.Errorf("failed to get latest commitID in the repository")
}
return &items.Value[0], nil
}
@@ -202,7 +216,7 @@ func parseUrl(rawUrl string) (*azureOptions, error) {
return nil, errors.Errorf("supported url schemes are https and ssh; recevied URL %s rawUrl", rawUrl)
}
var expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
const expectedSshUrl = "git@ssh.dev.azure.com:v3/Organisation/Project/Repository"
func parseSshUrl(rawUrl string) (*azureOptions, error) {
path := strings.Split(rawUrl, "/")
@@ -340,6 +354,7 @@ func (a *azureClient) buildTreeUrl(config *azureOptions, rootObjectHash string)
if err != nil {
return "", errors.Wrapf(err, "failed to parse list tree url path %s", rawUrl)
}
q := u.Query()
// projectId={projectId}&recursive=true&fileName={fileName}&$format={$format}&api-version=6.0
q.Set("recursive", "true")
@@ -358,9 +373,11 @@ func formatReferenceName(name string) string {
if strings.HasPrefix(name, branchPrefix) {
return strings.TrimPrefix(name, branchPrefix)
}
if strings.HasPrefix(name, tagPrefix) {
return strings.TrimPrefix(name, tagPrefix)
}
return name
}
@@ -368,9 +385,11 @@ func getVersionType(name string) string {
if strings.HasPrefix(name, branchPrefix) {
return "branch"
}
if strings.HasPrefix(name, tagPrefix) {
return "tag"
}
return "commit"
}
@@ -396,7 +415,10 @@ func (a *azureClient) listRefs(ctx context.Context, opt baseOption) ([]string, e
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
}
resp, err := a.client.Do(req)
client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithMessage(err, "failed to make an HTTP request")
}
@@ -453,7 +475,10 @@ func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string,
return nil, errors.WithMessage(err, "failed to create a new HTTP request")
}
resp, err := a.client.Do(req)
client := newHttpClientForAzure(opt.tlsSkipVerify)
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
return nil, errors.WithMessage(err, "failed to make an HTTP request")
}
@@ -483,9 +508,10 @@ func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string,
func checkAzureStatusCode(err error, code int) error {
if code == http.StatusNotFound {
return ErrIncorrectRepositoryURL
return gittypes.ErrIncorrectRepositoryURL
} else if code == http.StatusUnauthorized || code == http.StatusNonAuthoritativeInfo {
return ErrAuthenticationFailure
return gittypes.ErrAuthenticationFailure
}
return err
}

View File

@@ -8,13 +8,13 @@ import (
"testing"
"time"
gittypes "github.com/portainer/portainer/api/git/types"
_ "github.com/joho/godotenv/autoload"
"github.com/stretchr/testify/assert"
)
var (
privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
)
const privateAzureRepoURL = "https://portainer.visualstudio.com/gitops-test/_git/gitops-test"
func TestService_ClonePublicRepository_Azure(t *testing.T) {
ensureIntegrationTest(t)
@@ -58,7 +58,7 @@ func TestService_ClonePublicRepository_Azure(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
dst := t.TempDir()
repositoryUrl := fmt.Sprintf(tt.args.repositoryURLFormat, tt.args.password)
err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "")
err := service.CloneRepository(dst, repositoryUrl, tt.args.referenceName, "", "", false)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
})
@@ -73,7 +73,7 @@ func TestService_ClonePrivateRepository_Azure(t *testing.T) {
dst := t.TempDir()
err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat)
err := service.CloneRepository(dst, privateAzureRepoURL, "refs/heads/main", "", pat, false)
assert.NoError(t, err)
assert.FileExists(t, filepath.Join(dst, "README.md"))
}
@@ -84,7 +84,7 @@ func TestService_LatestCommitID_Azure(t *testing.T) {
pat := getRequiredValue(t, "AZURE_DEVOPS_PAT")
service := NewService(context.TODO())
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat)
id, err := service.LatestCommitID(privateAzureRepoURL, "refs/heads/main", "", pat, false)
assert.NoError(t, err)
assert.NotEmpty(t, id, "cannot guarantee commit id, but it should be not empty")
}
@@ -96,7 +96,7 @@ func TestService_ListRefs_Azure(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := NewService(context.TODO())
refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false)
refs, err := service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
assert.NoError(t, err)
assert.GreaterOrEqual(t, len(refs), 1)
}
@@ -106,10 +106,10 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go service.ListRefs(privateAzureRepoURL, username, accessToken, false)
service.ListRefs(privateAzureRepoURL, username, accessToken, false)
go service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
service.ListRefs(privateAzureRepoURL, username, accessToken, false, false)
time.Sleep(2 * time.Second)
}
@@ -145,7 +145,7 @@ func TestService_ListFiles_Azure(t *testing.T) {
extensions: []string{},
expect: expectResult{
shouldFail: true,
err: ErrAuthenticationFailure,
err: gittypes.ErrAuthenticationFailure,
},
},
{
@@ -161,7 +161,7 @@ func TestService_ListFiles_Azure(t *testing.T) {
extensions: []string{},
expect: expectResult{
shouldFail: true,
err: ErrAuthenticationFailure,
err: gittypes.ErrAuthenticationFailure,
},
},
{
@@ -240,14 +240,14 @@ func TestService_ListFiles_Azure(t *testing.T) {
extensions: []string{},
expect: expectResult{
shouldFail: true,
err: ErrIncorrectRepositoryURL,
err: gittypes.ErrIncorrectRepositoryURL,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions)
paths, err := service.ListFiles(tt.args.repositoryUrl, tt.args.referenceName, tt.args.username, tt.args.password, false, tt.extensions, false)
if tt.expect.shouldFail {
assert.Error(t, err)
if tt.expect.err != nil {
@@ -268,10 +268,10 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), REPOSITORY_CACHE_SIZE, 200*time.Millisecond)
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{})
go service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}, false)
service.ListFiles(privateAzureRepoURL, "refs/heads/main", username, accessToken, false, []string{}, false)
time.Sleep(2 * time.Second)
}

View File

@@ -7,6 +7,7 @@ import (
"net/url"
"testing"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/stretchr/testify/assert"
)
@@ -291,7 +292,6 @@ func Test_azureDownloader_downloadZipFromAzureDevOps(t *testing.T) {
defer server.Close()
a := &azureClient{
client: server.Client(),
baseUrl: server.URL,
}
@@ -328,7 +328,6 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
defer server.Close()
a := &azureClient{
client: server.Client(),
baseUrl: server.URL,
}
@@ -441,6 +440,7 @@ func Test_listRefs_azure(t *testing.T) {
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
tests := []struct {
name string
args baseOption
@@ -466,7 +466,7 @@ func Test_listRefs_azure(t *testing.T) {
password: "test-token",
},
expect: expectResult{
err: ErrAuthenticationFailure,
err: gittypes.ErrAuthenticationFailure,
},
},
{
@@ -477,7 +477,7 @@ func Test_listRefs_azure(t *testing.T) {
password: "",
},
expect: expectResult{
err: ErrAuthenticationFailure,
err: gittypes.ErrAuthenticationFailure,
},
},
{
@@ -488,7 +488,7 @@ func Test_listRefs_azure(t *testing.T) {
password: accessToken,
},
expect: expectResult{
err: ErrIncorrectRepositoryURL,
err: gittypes.ErrIncorrectRepositoryURL,
},
},
}
@@ -540,7 +540,7 @@ func Test_listFiles_azure(t *testing.T) {
},
expect: expectResult{
shouldFail: true,
err: ErrAuthenticationFailure,
err: gittypes.ErrAuthenticationFailure,
},
},
{
@@ -555,7 +555,7 @@ func Test_listFiles_azure(t *testing.T) {
},
expect: expectResult{
shouldFail: true,
err: ErrAuthenticationFailure,
err: gittypes.ErrAuthenticationFailure,
},
},
{
@@ -599,7 +599,7 @@ func Test_listFiles_azure(t *testing.T) {
},
expect: expectResult{
shouldFail: true,
err: ErrIncorrectRepositoryURL,
err: gittypes.ErrIncorrectRepositoryURL,
},
},
}

64
api/git/backup.go Normal file
View File

@@ -0,0 +1,64 @@
package git
import (
"fmt"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/rs/zerolog/log"
)
var (
ErrInvalidGitCredential = errors.New("Invalid git credential")
)
type CloneOptions struct {
ProjectPath string
URL string
ReferenceName string
Username string
Password string
// TLSSkipVerify skips SSL verification when cloning the Git repository
TLSSkipVerify bool `example:"false"`
}
func CloneWithBackup(gitService portainer.GitService, fileService portainer.FileService, options CloneOptions) (clean func(), err error) {
backupProjectPath := fmt.Sprintf("%s-old", options.ProjectPath)
cleanUp := false
cleanFn := func() {
if !cleanUp {
return
}
err = fileService.RemoveDirectory(backupProjectPath)
if err != nil {
log.Warn().Err(err).Msg("unable to remove git repository directory")
}
}
err = filesystem.MoveDirectory(options.ProjectPath, backupProjectPath)
if err != nil {
return cleanFn, errors.WithMessage(err, "Unable to move git repository directory")
}
cleanUp = true
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify)
if err != nil {
cleanUp = false
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath)
if restoreError != nil {
log.Warn().Err(restoreError).Msg("failed restoring backup folder")
}
if err == gittypes.ErrAuthenticationFailure {
return cleanFn, errors.WithMessage(err, ErrInvalidGitCredential.Error())
}
return cleanFn, errors.WithMessage(err, "Unable to clone git repository")
}
return cleanFn, nil
}

Some files were not shown because too many files have changed in this diff Show More