Compare commits

...

432 Commits

Author SHA1 Message Date
James Player
e75e379b33 Bump version to 2.28.0 (#523) 2025-03-19 11:50:11 +13:00
Steven Kang
2791bd123c fix: cve-2025-22869 develop (#511) 2025-03-17 12:24:39 +13:00
andres-portainer
e1f9b69cd5 feat(edgestack): improve the structure to make JSON operations faster BE-11668 (#475) 2025-03-15 10:10:17 -03:00
andres-portainer
2c05496962 feat(edgeconfigs): parse .env config files for interpolation BE-11673 (#514) 2025-03-15 10:09:22 -03:00
Oscar Zhou
66bcf9223a fix(k8s/config): avoid hardcoded "insecure-skip-tls-verify" in kubeconfig [BE-11651] (#500) 2025-03-14 11:20:41 +13:00
James Player
993f69db37 chore(app): Migrate helm templates list to react (#492) 2025-03-14 10:37:14 +13:00
Ali
58317edb6d fix(namespaces): only show namespaces with access [r8s-251] (#501) 2025-03-14 07:57:06 +13:00
Steven Kang
417891675d fix: ensure no non-admin users have access to system namespaces (#499) 2025-03-13 16:43:56 +13:00
Steven Kang
8b7aef883a fix: display unscheduled applications (#496)
Co-authored-by: JamesPlayer <james.player@portainer.io>
2025-03-13 14:13:18 +13:00
Ali
b5961d79f8 refactor(helm): helm binary to sdk refactor [r8s-229] (#463)
Co-authored-by: stevensbkang <skan070@gmail.com>
2025-03-13 12:20:16 +13:00
LP B
0d25f3f430 fix(app): restore gitops update options (#419) 2025-03-12 14:00:31 +01:00
Steven Kang
798fa2396a feat: kubernets service - display external hostname (#486) 2025-03-12 22:34:00 +13:00
James Player
28b222fffa fix(app): Make sure empty tables don't have select all rows checkbox checked (#489) 2025-03-12 10:34:07 +13:00
James Player
b57855f20d fix(app): datatable global checkbox doesn't reflect the selected state (#470) 2025-03-10 09:21:20 +13:00
Cara Ryan
438b1f9815 fix(helm): Remove duplicate helm instructions in CE [BE-11670] (#482) 2025-03-06 09:35:31 +13:00
LP B
2bccb3589e fix(app/images): nodeName on images list links (#484) 2025-03-05 16:04:16 +01:00
James Player
52bb06eb7b chore(helm): Convert helm details view to react (#476) 2025-03-03 11:29:58 +13:00
Malcolm Lockyer
8e6d0e7d42 perf(endpointrelation): Part 2 of fixing endpointrelation perf [be-11616] (#471) 2025-02-28 14:41:54 +13:00
Steven Kang
5526fd8296 chore: bump 2.27.1 - develop (#468) 2025-02-27 11:02:25 +13:00
Anthony Lapenna
a554a8c49f api: remove server-ce swagger.json (#467) 2025-02-26 16:10:02 +13:00
James Player
7759d762ab chore(react): Convert cluster details to react CE (#466) 2025-02-26 14:13:50 +13:00
Oscar Zhou
dd98097897 fix(libstack): miss to read default .env file [BE-11638] (#458) 2025-02-26 13:00:25 +13:00
Steven Kang
cc73b7831f fix: cve-2024-50338 - develop (#461) 2025-02-25 12:55:44 +13:00
James Carppe
9c243cc8dd Update bug report template for 2.27.0 (#450) 2025-02-20 13:38:26 +13:00
Oscar Zhou
5d568a3f32 fix(edge): edge stack pending when yaml file is under same root folder of edge configs [BE-11620] (#447) 2025-02-20 12:09:26 +13:00
Steven Kang
1b83542d41 chore: bump version to 2.27.0 - develop (#445) 2025-02-20 09:42:52 +13:00
LP B
cf95d91db3 fix(swarm): keep swarm stack stop command attached (#444) 2025-02-19 19:25:28 +01:00
Viktor Pettersson
41c1d88615 fix(edge): configure persisted mTLS certificates on start-up [BE-11622] (#437)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
Co-authored-by: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com>
2025-02-19 14:46:39 +13:00
Steven Kang
df8673ba40 version: bump version to 2.27.0-rc3 - develop (#426) 2025-02-14 08:39:02 +13:00
andres-portainer
96b1869a0c fix(swarm): fix the Host field when listing images BE-10827 (#352)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2025-02-12 00:47:45 +01:00
Oscar Zhou
e45b852c09 fix(platform): remove error log when local env is not found [BE-11353] (#364) 2025-02-12 09:23:52 +13:00
Steven Kang
2d3e5c3499 workaround: leave the globally set helm repo to empty and add disclaimer - develop (#409) 2025-02-11 15:36:29 +13:00
Oscar Zhou
b25bf1e341 fix(podman): missing filter in homepage [BE-11502] (#404) 2025-02-10 21:08:27 +13:00
Oscar Zhou
4bb80d3e3a fix(setting): failed to persist edge computer setting [BE-11403] (#395) 2025-02-10 21:05:15 +13:00
Steven Kang
03575186a7 remove deprecated api endpoints - develop [BE-11510] (#399) 2025-02-10 10:46:36 +13:00
Steven Kang
935c7dd496 feat: improve diagnostics stability - develop (#355) 2025-02-10 10:45:47 +13:00
Steven Kang
1b2dc6a133 version: bump version to 2.27.0-rc2 - develop (#402) 2025-02-07 14:47:49 +13:00
Steven Kang
d4e2b2188e chore: bump go version to 1.23.5 develop (#392) 2025-02-07 08:48:19 +13:00
viktigpetterr
9658f757c2 fix(endpoints): use the post method for batch delete API operations [BE-11573] (#394) 2025-02-06 18:14:43 +01:00
Ali
371e84d9a5 fix(podman): create new image from a container in podman [r8s-90] (#347) 2025-02-05 20:22:33 +13:00
Steven Kang
5423a2f1b9 security: cve-2025-21613 develop (#390) 2025-02-05 15:56:30 +13:00
Oscar Zhou
7001f8e088 fix(edge): check all endpoint_relation db query logic [BE-11602] (#378) 2025-02-05 15:20:20 +13:00
Steven Kang
678cd54553 security: cve-2024-45338 develop (#386) 2025-02-05 15:03:39 +13:00
Oscar Zhou
bc19d6592f fix(libstack): cannot open std edge stack log page [BE-11603] (#384) 2025-02-05 12:17:51 +13:00
James Player
5af0859f67 fix(datatables): "Select all" should select only elements of the current page (#376) 2025-02-04 15:34:33 +13:00
Oscar Zhou
379711951c fix(edgegroup): failed to associate env to static edge group [BE-11599] (#368) 2025-02-04 09:41:24 +13:00
LP B
a50a9c5617 fix(app/edge): edge stacks webhooks cannot be disabled once created (#372) 2025-02-03 20:50:24 +01:00
LP B
c0d30a455f fix(api/edge): backend panic on edge stack removal (#371) 2025-02-03 20:25:25 +01:00
LP B
9a3f6b21d2 feat(app/service-details): hide view while loading data (#348) 2025-02-03 14:20:35 +01:00
Steven Kang
9ea41f68bc version: bump version to 2.27.0-rc1 (#363)
Co-authored-by: steven <steven@stevens-Mini.hub>
2025-02-03 11:38:38 +13:00
James Player
e943aa8f03 feat(documentation): change docs to use LTS/STS instead of version number (#357) 2025-02-03 11:17:36 +13:00
James Player
17a4750d8e fix(kubernetes): Resource reservation wasn't displaying properly in business edition and remove leader status (#362) 2025-02-03 11:02:23 +13:00
Malcolm Lockyer
7d18c22aa1 fix(ui): bring back k8s applications page row expand published urls [r8s-145] (#356) 2025-01-31 13:16:18 +13:00
Ali
c80cc6e268 chore(automation): give unique selectors [r8s-168] (#345)
Co-authored-by: JamesPlayer <james.player@portainer.io>
2025-01-30 15:42:32 +13:00
andres-portainer
b30a1b5250 fix(edgestacks): avoid repeated statuses BE-11561 (#351) 2025-01-27 16:00:05 -03:00
LP B
b753371700 fix(app/edge-stack): edge stack create form validation (#343) 2025-01-24 17:02:52 +01:00
andres-portainer
3ca5ab180f fix(system): optimize the memory usage when counting nodes BE-11575 (#342) 2025-01-23 20:41:09 -03:00
Ali
4971f5510c fix(app): edit app with configmap [r8s-95] (#341) 2025-01-24 11:35:47 +13:00
andres-portainer
20fa7e508d fix(edgestacks): decouple the EdgeStackStatusUpdateCoordinator so it can be used by other packages BE-11572 (#340) 2025-01-23 17:10:46 -03:00
James Player
ebffc340d9 fix(k8s): Changed 'Deploy from file' button text to 'Deploy from code' (#338) 2025-01-23 16:47:52 +13:00
andres-portainer
9a86737caa fix(edgestacks): add a status update coordinator to increase performance BE-11572 (#337) 2025-01-22 20:24:54 -03:00
Steven Kang
d35d8a7307 feat(oauth): fix mapping (#330) 2025-01-23 09:03:51 +13:00
andres-portainer
701ff5d6bc refactor(edgestacks): move handlerDBErr() out of the handler BE-11572 (#336) 2025-01-22 16:35:06 -03:00
LP B
9044b25a23 fix(app): remove passwords from registries list response (#334) 2025-01-22 17:40:21 +01:00
Ali
7f089fab86 fix(apps): use replicas from application spec [r8s-142] (#335) 2025-01-22 12:31:27 +13:00
James Carppe
a259c28678 Update bug report template for 2.26.1 (#329) 2025-01-21 16:19:03 +13:00
LP B
db48da185a fix(app/editor): reduce editor slowness by debouncing onChange calls (#326) 2025-01-17 22:41:06 +01:00
LP B
cab667c23b fix(app/edge-stack): UI notification on creation error (#325) 2025-01-17 20:33:01 +01:00
andres-portainer
154ca9f1b1 fix(edge): return proper error from context BE-11564 (#323) 2025-01-16 20:18:51 -03:00
Oscar Zhou
2abe40b786 fix(edgestack): remove project folder after deleting edgestack [BE-11559] (#320) 2025-01-16 09:16:09 +13:00
James Carppe
6be2420b32 Update bug report template for 2.26.0 (#319) 2025-01-15 14:38:59 +13:00
Ali
9405cc0e04 chore(portainer): bump version to 2.26.0 (#302) 2025-01-14 07:20:11 +13:00
Yajith Dayarathna
55c98912ed feat(omni): support for omni [R8S-75] (#105)
Co-authored-by: stevensbkang <skan070@gmail.com>
Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: Malcolm Lockyer <segfault88@users.noreply.github.com>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
2025-01-13 17:06:10 +13:00
Ali
45bd7984b0 fit(jobs): remove redundant checkboxes in executions datatable [r8s-182] (#295) 2025-01-12 18:24:22 +13:00
andres-portainer
1ed9a0106e feat(edge): optimize Edge Stack retrieval BE-11555 (#294) 2025-01-10 16:44:19 -03:00
LP B
f8b2ee8c0d fix(app/edge-stack): local filesystem path is not retained (#292) 2025-01-10 18:20:44 +01:00
Steven Kang
d32b0f8b7e feat(kubernetes): support for jobs and cron jobs - r8s-182 (#260)
Co-authored-by: James Carppe <85850129+jamescarppe@users.noreply.github.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: andres-portainer <91705312+andres-portainer@users.noreply.github.com>
Co-authored-by: Oscar Zhou <100548325+oscarzhou-portainer@users.noreply.github.com>
Co-authored-by: Yajith Dayarathna <yajith.dayarathna@portainer.io>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
Co-authored-by: testA113 <aliharriss1995@gmail.com>
2025-01-10 13:21:27 +13:00
andres-portainer
24fdb1f600 fix(libstack): redirect the Docker and Compose logging to zerolog BE-11518 (#289) 2025-01-08 16:26:04 -03:00
Oscar Zhou
4010174f66 fix(docker/volume): failed to list volume before snapshot is created [BE-11544] (#286) 2025-01-08 09:45:13 +13:00
andres-portainer
e2b812a611 fix(edgestacks): check the version of the edge stack before updating the status BE-11488 (#287) 2025-01-07 17:31:57 -03:00
andres-portainer
d72b3a9ba2 feat(edgestacks): optimize the Edge Stack status update endpoint BE-11539 (#279) 2025-01-06 15:39:24 -03:00
LP B
85f52d2574 feat(app/stack): ability to prune volumes on stack/edge stack delete (#232)
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
2025-01-01 10:44:49 +13:00
andres-portainer
33ea22c0a9 feat(ssl): improve caching behavior BE-11527 (#273) 2024-12-30 11:10:13 -03:00
andres-portainer
0d52f9dd0e feat(async): avoid sending CSRF token for async edge polling requests BE-1152 (#272) 2024-12-30 10:58:44 -03:00
andres-portainer
3caffe1e85 feat(async): filter out Docker snapshot diffs without meaningful changes BE-11527 (#265) 2024-12-26 18:45:20 -03:00
Oscar Zhou
87b8dd61c3 fix: replace strings.ToLower with strings.EqualFold [BE-11524] (#263) 2024-12-24 11:15:16 +13:00
andres-portainer
ad77cd195c fix(docker): fix a data race in the Docker transport BE-10873 (#255) 2024-12-23 09:54:11 -03:00
James Carppe
eb2a754580 Update bug report template for 2.21.5 / 2.25.1 (#261) 2024-12-20 14:39:33 +13:00
Steven Kang
9258db58db feat(auth): add 30m session timeout - r8s-178 (#259) 2024-12-20 10:49:13 +13:00
andres-portainer
8d1c90f912 fix(platform): fix a data race in GetPlatform() BE-11522 (#253) 2024-12-19 09:37:50 -03:00
Steven Kang
1c62bd6ca5 fix: security - CVE-2024-45337 - portainer-suite develop (#247) 2024-12-19 10:55:34 +13:00
andres-portainer
13317ec43c feat(stacks): simplify WaitForStatus() BE-11505 (#241) 2024-12-17 16:25:49 -03:00
James Carppe
35dcb5ca46 Update bug report template for 2.25.0 (#245) 2024-12-16 13:53:15 +13:00
AndrewHucklesby
4454b6b890 bump version to 2.25.0 (#240) 2024-12-12 16:42:55 +13:00
Ali
117e3500ae fix(edge-stack): revert useEffect, to call matchRegistry less often [BE-11501] (#239) 2024-12-12 15:22:19 +13:00
andres-portainer
94fda6a720 fix(offlinegate): avoid leaking an RLock when the handler panics BE-11495 (#234) 2024-12-11 16:38:03 -03:00
Ali
e1388eff84 fix(annotations): parse annotation keys in angular forms [r8s-170] (#233) 2024-12-11 17:50:08 +13:00
Ali
94d2e32b49 fix(apps): simplify helm status [r8s-155] (#230) 2024-12-11 13:18:34 +13:00
Ali
069f22afa4 fix(services): separate table state [BE-11401] (#152) 2024-12-11 11:58:43 +13:00
LP B
52c90d4d0a feat(app/edge-stack): ability to prune containers on edge stack update (#216) 2024-12-10 22:54:02 +01:00
Ali
ce7e0d8d60 refactor(namespace): migrate namespace edit to react [r8s-125] (#38) 2024-12-11 10:15:46 +13:00
Oscar Zhou
40c7742e46 fix(edgestack): validate edge stack name for api [BE-11365] (#222) 2024-12-11 08:21:46 +13:00
Malcolm Lockyer
05e872337a feat(support): add db and activity db file size to support bundle [r8s-169] (#221) 2024-12-10 09:35:30 +13:00
Ali
aac9d001f7 feat(askai): hide askAI for CE [BE-11409] (#220) 2024-12-10 09:11:51 +13:00
andres-portainer
d295968948 feat(libstack): update Compose to v2.31.0 BE-11416 (#223) 2024-12-09 16:36:57 -03:00
Ali
97e7a3c5e2 fix(edge-stacks): various custom template issues [BE-11414] (#189) 2024-12-09 17:48:34 +13:00
Ali
16a1825990 feat(version): remove brackets for sts/lts [BE-11409] (#215) 2024-12-06 22:52:47 +13:00
Ali
441afead10 feat(ask-ai): integrate kapa-ai page [BE-11409] (#214) 2024-12-06 18:41:32 +13:00
Malcolm Lockyer
783ab253af feat(support): collect system info bundle to assist support troubleshooting [r8s-157] (#154) 2024-12-06 15:38:10 +13:00
Yajith Dayarathna
17648d12fe codecov integration with portainer-suite [PLA-119] (#210) 2024-12-06 12:09:09 +13:00
andres-portainer
2f4f1be99c feat(performance): increase HTTP compression performance BE-11417 (#211) 2024-12-05 19:10:56 -03:00
Ali
5d4d3888b8 fix(rbac): use team ids to get namespace access [r8s-154] (#209) 2024-12-05 17:29:45 +13:00
andres-portainer
473084e915 fix(edgestacks): remove edge stacks even after a system crash or power-off BE-10822 (#208) 2024-12-04 19:52:53 -03:00
Anthony Lapenna
a8147b9713 build: tidy up packages by removing unused scripts and files (#207) 2024-12-05 11:18:49 +13:00
Yajith Dayarathna
3c3dc547b2 fix(app/edge-stack): hide non-working BE fields from CE (#205)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2024-12-04 19:00:40 +01:00
James Carppe
c5accd0f16 Update bug report template for 2.24.1 (#191) 2024-12-04 08:34:59 +13:00
Oscar Zhou
cb949e443e fix(volume): unable to inspect and browse volume [BE-11216] (#186) 2024-12-03 09:10:10 +13:00
Anthony Lapenna
bb6815f681 build: introduce central Makefile and live-reload for Go (#184) 2024-12-03 08:49:03 +13:00
Anthony Lapenna
a261f60764 version: display dependencies versions (#188)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2024-12-03 08:45:44 +13:00
LP B
d393529026 fix(app): passing an initial table state overrides the default global filter state (#180) 2024-11-29 21:06:11 +01:00
Oscar Zhou
219c9593e0 fix(container): binding ip disappear after duplicate container [BE-11413] (#177) 2024-11-29 08:56:44 +13:00
andres-portainer
faa6b2b790 fix(libstack): add the build step for Compose BE-11448 (#173) 2024-11-27 18:43:25 -03:00
Oscar Zhou
4046bf7b31 feat(image): build image with file [BE-11372] (#171) 2024-11-27 18:33:35 -03:00
Ali
4f708309af fix(activity logs): decode base64 [BE-11418] (#172) 2024-11-28 08:54:32 +13:00
andres-portainer
f2e7680bf3 fix(compose): fix path resolution for env files BE-11428 (#167) 2024-11-26 22:09:58 -03:00
andres-portainer
5d2689b139 fix(compose): avoid creating a default network unnecessarily BE-11427 (#169) 2024-11-26 19:48:49 -03:00
andres-portainer
145ffeea40 fix(libstack): resolve env vars correctly in Compose BE-11420 (#166) 2024-11-26 18:09:12 -03:00
andres-portainer
13143bc7ea fix(libstack): fix environment variable handling in compose BE- (#165) 2024-11-26 17:37:22 -03:00
Oscar Zhou
ee0dbf2d22 feat(init): allow to customize kubectl-shell image by cli flag [BE-11419] (#162) 2024-11-26 10:17:46 +13:00
andres-portainer
4265ae4dae feat(offlinegate): improve error message BE-11402 (#163) 2024-11-25 17:40:17 -03:00
andres-portainer
821c1fdbef feat(swarm): do not prevent server startup when Swarm config.json file is invalid BE-11402 (#160) 2024-11-25 17:40:10 -03:00
andres-portainer
fe29d6aee3 feat(backup): reduce the locking time of the offline gate BE-11402 (#157) 2024-11-25 10:10:11 -03:00
Ali
c0c7144539 fix(app templates): load app template for deployment [BE-11382] (#141) 2024-11-25 17:41:09 +13:00
Anthony Lapenna
20e3d3a15b fix: review snapshot and post init migration logic (#158) 2024-11-25 11:03:12 +13:00
James Carppe
07d1eedae3 Update template to include lifecycle policy link (#156) 2024-11-21 17:11:20 +13:00
James Carppe
4ad3d70739 Update bug report template for 2.24.0 (#153) 2024-11-20 13:15:56 +13:00
andres-portainer
e6a1c29655 fix(compose): fix support for ECR BE-11392 (#151) 2024-11-18 16:42:53 -03:00
Yajith Dayarathna
333dfe1ebf refactor(edge/update): choose images from registry [BE-10964] (#6)
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
2024-11-18 14:11:26 +13:00
andres-portainer
c59872553a fix(stacks): pass the registry credentials to Compose stacks BE-11388 (#147)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2024-11-18 08:39:13 +13:00
andres-portainer
1a39370f5b fix(libstack): add missing private registry credentials BE-11388 (#143) 2024-11-15 17:38:55 -03:00
Oscar Zhou
bc44056815 fix(swarm): failed to deploy app template [BE-11385] (#138) 2024-11-15 11:53:22 +13:00
andres-portainer
17c92343e0 fix(compose): avoid leftovers in Run() BE-11381 (#129) 2024-11-13 20:24:20 -03:00
andres-portainer
cd6935b07a feat(edgestacks): add a retry period to edge stack deployments BE-11155 (#109)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2024-11-13 20:13:30 -03:00
andres-portainer
47d428f3eb fix(libstack): fix compose run BE-11381 (#126) 2024-11-13 14:38:53 -03:00
LP B
2baae7072f fix(edge/stacks): use default namespace when none is specified in manifest (#124) 2024-11-13 16:30:08 +13:00
andres-portainer
2e9e459aa3 fix(libstack): add a different timeout for WaitForStatus BE-11376 (#120) 2024-11-12 19:31:44 -03:00
andres-portainer
7444e2c1c7 fix(compose): provide the project name for proper validation BE-11375 (#118) 2024-11-12 17:18:40 -03:00
Oscar Zhou
d6469eb33d fix(libstack): empty project name [BE-11375] (#116) 2024-11-12 10:20:45 -03:00
Ali
a2da6f1827 fix(configmap): create portainer configmap if it doesn't exist [r8s-141] (#113) 2024-11-12 18:23:00 +13:00
Oscar Zhou
e6508140f8 version: bump version to 2.24.0 (#102) 2024-11-12 12:13:27 +13:00
andres-portainer
a7127bc74f feat(libstack): remove the docker-compose binary BE-10801 (#111)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
2024-11-11 19:05:56 -03:00
Malcolm Lockyer
55aa0c0c5d fix(ui): kubernetes create from file page - fix template load failed mistake in ce (#112) 2024-11-12 10:46:37 +13:00
Ali
d25de4f459 fix(more-resources): address CE review comments [r8s-103] (#110) 2024-11-12 10:41:43 +13:00
Yajith Dayarathna
6d31f4876a fix(more resources): fix porting and functionality [r8s-103] (#8)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
2024-11-12 09:55:30 +13:00
Steven Kang
e6577ca269 kubernetes: improved the node view [r8s-47] (#108) 2024-11-12 09:42:14 +13:00
Ali
08d77b4333 fix(namespace): handle no accesses found [r8s-141] (#106) 2024-11-12 09:29:55 +13:00
Ali
1ead121c9b fix(apps): for helm uninstall, ignore manual associated resource deletion [r8s-124] (#103) 2024-11-12 09:03:22 +13:00
LP B
ad19b4a421 fix(app): relocate Skip TLS switch next to git repo URL field (#107) 2024-11-11 17:16:37 +01:00
LP B
6bc52dd39c feat(edge): kubernetes WaitForStatus support (#85) 2024-11-11 14:02:20 +01:00
Malcolm Lockyer
fd2b00bf3b fix(ui): kubernetes create from file page - fix template load failed message style [R8S-68] (#95) 2024-11-11 12:06:56 +13:00
Ali
cd8c6d1ce0 fix(apps): don't delete the 'kubernetes' service or duplicate service names [r8s-124] (#90) 2024-11-11 08:26:56 +13:00
Ali
e9fc6d5598 refactor(namespace): migrate namespace access view to react [r8s-141] (#87) 2024-11-11 08:17:20 +13:00
Steven Kang
8ed7cd80cb feat(ui): improve Kubernetes node view [r8s-47] (#84) 2024-11-07 14:10:19 +13:00
Malcolm Lockyer
81322664ea fix(ui): kubernetes create from manifest page misalignments and incorrect loading icon [R8S-68] (#88) 2024-11-07 09:04:24 +13:00
Ali
458d722d47 fix(ui): consistent widget padding [r8s-136] (#82) 2024-11-05 14:25:40 +13:00
Malcolm Lockyer
3c0d25f3bd fix(ui): rename create from manifest to create from file [BE-11335] (#86) 2024-11-05 14:10:08 +13:00
Oscar Zhou
ca7e4dd66e fix(edge/async): onboarding agent without predefined group cannot be associated [BE-11281] (#83) 2024-11-05 09:32:25 +13:00
Ali
c1316532eb fix(apps): update associated resources on deletion [r8s-124] (#75) 2024-11-01 21:03:49 +13:00
Ali
d418784346 fix(rbac): revert rbac detection logic [r8s-137] (#81) 2024-11-01 19:28:23 +13:00
andres-portainer
1061601714 feat(activity-log): set descending timestamps as the default sorting order BE-11343 (#66) 2024-10-31 18:07:26 -03:00
andres-portainer
2f3d4a5511 fix(activity-log): fix broken sorting BE-11342 (#65) 2024-10-31 17:25:38 -03:00
LP B
9ea62bda28 fix(app/image-details): export images to tar (#40) 2024-10-31 17:40:01 +01:00
Steven Kang
94b1d446c0 fix(ingresses): load cluster wide ingresses [r8s-78] (#78) 2024-10-31 13:08:09 +13:00
Ali
6c57a00a65 fix(cluster): UI RBAC alert fix [r8s-138] (#72) 2024-10-31 10:12:56 +13:00
Yajith Dayarathna
8808531cd5 update ci trigger paths for portainer-ee - develop (#68) 2024-10-29 12:23:31 +13:00
andres-portainer
966fca950b fix(oauth): add a timeout to getOAuthToken() BE-11283 (#63) 2024-10-28 17:28:22 -03:00
Yajith Dayarathna
e528cff615 bump golang version to 1.23.2 (#60) 2024-10-29 09:02:18 +13:00
andres-portainer
1d037f2f1f feat(websocket): improve websocket code sharing BE-11340 (#61) 2024-10-25 11:21:49 -03:00
James Carppe
b2d67795b3 Update bug report template for 2.21.4 (#62) 2024-10-25 15:49:31 +13:00
Ali
959c527be7 refactor(apps): migrate applications view to react [r8s-124] (#28) 2024-10-25 12:28:05 +13:00
andres-portainer
cc75167437 fix(swarm): fix service updates BE-11219 (#57) 2024-10-23 18:23:24 -03:00
andres-portainer
3114d4b5c5 fix(security): add initial support for HSTS and CSP BE-11311 (#47) 2024-10-21 13:52:11 -03:00
andres-portainer
ac293cda1c feat(database): share more database code between CE and EE BE-11303 (#43) 2024-10-18 10:33:10 -03:00
Ali
7b88975bcb fix(applications): scale resource usage by pod count [r8s-127] (#33) 2024-10-16 14:33:45 +13:00
James Carppe
da4b2e3a56 Updated bug report template for 2.23.0 (#32) 2024-10-16 09:23:02 +13:00
andres-portainer
369598bc96 Bump version to v2.23.0 (#29) 2024-10-14 13:55:11 -03:00
andres-portainer
61c5269353 fix(edgejobs): decouple the Edge Jobs from the reverse tunnel service BE-10866 (#11) 2024-10-14 10:37:13 -03:00
LP B
7a35b5b0e4 refactor(ui/code-editor): accept enum type (#22)
Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portainer.io>
2024-10-14 13:52:51 +02:00
Yajith Dayarathna
20e9423390 chore: standalone repository workflow cleanup (#26) 2024-10-14 18:34:08 +13:00
Ali
cf230a1cbc fix(k8s-volumes): add missing json labels tag [r8s-108] (#27) 2024-10-14 13:37:59 +13:00
Ali
a06a09afcf fix(app): use standard resource request units [r8s-122] (#15) 2024-10-14 11:27:22 +13:00
Yajith Dayarathna
c88382ec1f fix(apps): persist table settings [r8s-120] (#10)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
2024-10-14 11:27:04 +13:00
Ali
fd0bc652a9 fix(volumes): update external labels CE [r8s-108] (#7) 2024-10-14 10:48:13 +13:00
Ali
57e10dc911 fix(apps): group helm apps together [r8s-102] (#24) 2024-10-14 10:28:56 +13:00
Yajith Dayarathna
1110f745e1 fix(volumes): allow standard users to select volumes [r8s-109] (#9)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
2024-10-12 13:01:27 +13:00
Oscar Zhou
811d03a419 chore: rm old .vscode.example folders in sub-repo [BE-11287] (#17)
Co-authored-by: deviantony <anthony.lapenna@portainer.io>
2024-10-11 16:10:16 +02:00
andres-portainer
666c031821 fix(git): optimize the git cloning process in terms of space BE-11286 (#20) 2024-10-10 18:49:50 -03:00
andres-portainer
4e457d97ad fix(linters): add back removed linters and extend them to CE BE-11294 2024-10-10 17:05:03 -03:00
andres-portainer
364e4f1b4e fix(linters): add back removed linters and extend them to CE BE-11294 2024-10-10 12:06:20 -03:00
andres-portainer
8aae557266 fix(stacks): run webhooks in background to avoid GitHub timeouts BE-11260 2024-10-09 17:28:19 -03:00
Yajith Dayarathna
2bd880ec29 required changes to enable monorepo.
Co-authored-by: deviantony <anthony.lapenna@portainer.io>
Co-authored-by: Yajith Dayarathna <yajith.dayarathna@portainer.io>
2024-10-09 08:37:23 +13:00
Oscar Zhou
b14438fd99 fix(edge): add agent id/name into edge api response [BE-10988] (#12256) 2024-10-08 19:17:09 +13:00
James Carppe
ba96d8a5fb Update bug report template for 2.21.3 (#12309) 2024-10-08 16:24:16 +13:00
Ali
db4b1dd024 fix(app): fix cpu type for decimals [r8s-107] (#12306) 2024-10-08 11:44:22 +13:00
Ali
469a4e94c2 fix(volumes): update the external, unused badges and used by col [r8s-105] (#12302) 2024-10-08 11:41:47 +13:00
Ali
44d6c0885e fix(node): call node usage [r8s-106] (#12304) 2024-10-08 11:39:05 +13:00
andres-portainer
9ce4ac9c9e fix(oauth): change the logging level from Debug to Error BE-4583 (#12305) 2024-10-07 18:21:05 -03:00
James Carppe
b40d22dc74 Update bug report template for 2.22.0 (#12283) 2024-10-03 14:53:37 +13:00
Steven Kang
a257696c25 fix access conditions when the restrict default namespace is enabled (#12280) 2024-10-02 15:55:05 +13:00
andres-portainer
f742937359 fix(endpoints): optimize the search performance BE-11267 (#12262) 2024-10-01 15:13:54 -03:00
Steven Kang
c0db48b29d fix ingress creation for none class (#12273) 2024-10-01 14:43:46 +13:00
Steven Kang
ea228c3d6d refactor(k8s): namespace core logic (#12142)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: James Carppe <85850129+jamescarppe@users.noreply.github.com>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
2024-10-01 14:15:51 +13:00
Ali
da010f3d08 fix(podman): ensure initial env type matches container runtime [r8s-98] (#12259) 2024-09-30 09:16:24 +13:00
Ali
32e94d4e4e feat(podman): support add podman envs in the wizard [r8s-20] (#12056) 2024-09-25 11:55:07 +12:00
Ali
db616bc8a5 fix(wizard): update nodeport placeholder [r8s-62] (#12255) 2024-09-25 11:36:50 +12:00
James Carppe
b8b46ec129 Update bug report template for 2.21.2 (#12251) 2024-09-24 11:42:15 +12:00
LP B
7d0b79a546 fix(app/images): export images to tar (#12223) 2024-09-23 21:55:45 +02:00
LP B
fd26565b14 fix(app/templates): non admins cannot load templates list (#12235) 2024-09-23 17:54:32 +02:00
Nik Wakelin
e0b6f2283a chore(branding): Changes Linode to Akamai Connected Cloud (#12221) 2024-09-23 09:21:02 +12:00
Oscar Zhou
d3d3d50569 fix(version): add specific version for updater image [BE-11153] (#12227) 2024-09-21 14:54:08 +12:00
andres-portainer
cee997e0b3 fix(edgestacks): reorder operations to properly update the endpoint relations BE-11233 (#12239) 2024-09-20 19:10:28 -03:00
LP B
80f53ed6ec fix(api): skip guessing env when there is no env in DB (#12238) 2024-09-20 17:56:41 -03:00
Chaim Lev-Ari
6f84317e7a feat(system): upgrade on swarm [EE-5848] (#11728)
Co-authored-by: Chaim Lev-Ari <chaim.levi-ari@portainer.io>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2024-09-20 18:00:38 +02:00
LP B
3cb484f06a fix(app/users): password validation hint + missing message on empty teams list (#12231) 2024-09-20 16:33:13 +02:00
LP B
61353cbe8a fix(app/edge): race between redirects when selecting a template (#12230) 2024-09-20 16:00:40 +02:00
Yajith Dayarathna
d647980c3a updating attest params (#12228) 2024-09-20 11:48:32 +12:00
Oscar Zhou
5740abe31b fix(authorization): add registry button disappear for admin [BE-11228] (#12213) 2024-09-20 08:18:51 +12:00
andres-portainer
5fd4f52e35 fix(jwt): fix handling of non-expiring JWT tokens BE-11242 (#12220) 2024-09-17 18:23:33 -03:00
Yajith Dayarathna
dbe7cd16d4 2024-09-CVE (#12189) 2024-09-11 11:08:46 +12:00
Yajith Dayarathna
2b630ca2dd enabling build attestations (#12211) 2024-09-11 10:57:52 +12:00
Oscar Zhou
2ede22646b fix(version): add specific version for updater image [BE-11153] (#12202) 2024-09-11 08:29:23 +12:00
James Carppe
994b6bb471 Update bug report template for 2.21.1 (#12207) 2024-09-10 14:33:32 +12:00
andres-portainer
92f338e0cd fix(users): fix data-race in userCreate() BE-11209 (#12193) 2024-09-05 22:28:04 -03:00
andres-portainer
7a176cf284 fix(teams): fix data-race in teamCreate() BE-11210 (#12195) 2024-09-05 21:36:13 -03:00
Oscar Zhou
80e607ab30 fix(stack): env placeholder as host path [BE-11187] (#12192) 2024-09-06 08:43:12 +12:00
Anthony Lapenna
6cff21477e service: update stop grace period description (#12173) 2024-09-05 08:47:06 +02:00
Yajith Dayarathna
4bb5a7f480 updating ci workflow (#12183) 2024-09-05 09:19:36 +12:00
andres-portainer
9a88511d00 fix(docker): avoid specifying the MAC address of container for Docker API < v1.44 BE-10880 (#12179) 2024-09-03 10:31:24 -03:00
Yajith Dayarathna
48cd614948 CVE 2024 43798 (#12171) 2024-09-03 09:27:24 +12:00
andres-portainer
2fe252d62b fix(jwt): generate JWT IDs BE-11179 (#12175) 2024-09-02 12:06:39 -03:00
LP B
8fae7f8438 feat(app/wizard): info panel telling to add env only once per swarm cluster (#11954) 2024-09-02 14:22:07 +02:00
andres-portainer
e4e55157e8 fix(bouncer): add support for JWT revocation BE-11179 (#12164) 2024-08-30 20:24:05 -03:00
Yajith Dayarathna
a5e246cc16 testing go directive change (#12124) 2024-08-30 08:27:42 +02:00
andres-portainer
d28dc59584 fix(git): optimize listFiles() BE-11184 (#12160) 2024-08-29 19:01:51 -03:00
andres-portainer
5353570721 task(code): remove unnecessary uses of govalidator BE-11181 (#12156) 2024-08-28 19:37:20 -03:00
andres-portainer
eb3e367ba8 fix(edgestacks): change the level of a logged line EE-6874 (#11396) 2024-08-28 18:16:34 -03:00
Chaim Lev-Ari
3c1441d462 refactor(users): migrate list view to react [EE-2202] (#11914) 2024-08-28 17:04:32 -03:00
Chaim Lev-Ari
33ce841040 refactor(docker/events): migrate list view to react [EE-2228] (#11581) 2024-08-28 16:41:15 -03:00
Chaim Lev-Ari
9797201c2a feat(docker): label gpu as nvidia only [EE-6999] (#11729) 2024-08-28 16:38:27 -03:00
Chaim Lev-Ari
6e14ac583b fix(access-control): fix dt column header typo [EE-7113] (#11853) 2024-08-28 16:37:12 -03:00
Anthony Lapenna
0b37b677c1 refactor: fix linting issues across the codebase (#12152) 2024-08-28 15:03:15 +02:00
Oscar Zhou
f59dd34154 fix(swarm/service): list task when filtering service [BE-11029] (#12146) 2024-08-28 18:28:38 +12:00
James Carppe
e8ec648886 Update bug report template for 2.21.0 (#12145) 2024-08-27 16:42:49 +12:00
Ali
10767a06df fix(invalidate): keep invalidate default behaviour [BE-11064] (#12080) 2024-08-27 09:48:50 +12:00
James Carppe
59b3375b59 Update bug report template for 2.21.0-rc2 (#12128) 2024-08-23 10:55:43 +12:00
andres-portainer
4408fd0cd3 chore(polling): simplify the polling logic BE-4585 (#12121) 2024-08-22 10:54:34 -03:00
Yajith Dayarathna
975a9517b9 undo change to go directive 2024-08-22 16:21:13 +12:00
Yajith Dayarathna
89c92b7834 updating go directive 2024-08-22 16:17:28 +12:00
Anthony Lapenna
747cea8084 security: bump dependencies to address CVEs (#12119) 2024-08-21 20:08:25 +12:00
Ali
f016b31388 fix(docker-desktop): support auth cookies [BE-11134] (#12108) 2024-08-21 18:21:51 +12:00
Oscar Zhou
8cd53a4b7a fix(registry): non admin can see add registry button [BE-10834] (#12112) 2024-08-21 11:00:00 +12:00
LP B
a39abe61c2 fix(api/edge_stacks): ensure edge stacks related endpoints list generation returns unique elements (#12101) 2024-08-20 10:20:03 +02:00
James Carppe
054898f821 Update bug report template for 2.21.0-rc1 (#12104) 2024-08-15 19:27:24 +12:00
Oscar Zhou
13d9b12a2e fix(group): create group twice when associating devices [EE-7418] (#12092) 2024-08-12 17:09:49 +12:00
LP B
aaec856282 fix(app/registries): enforce user accesses on registries (#12087) 2024-08-10 11:53:16 +02:00
andres-portainer
009eec9475 fix(compose): avoid the need to pass the file to remove the stack BE-11057 (#12065)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: Yajith Dayarathna <yajith.dayarathna@portainer.io>
2024-08-09 10:22:31 -03:00
Yajith Dayarathna
8d14535fd5 updating github workflow 2024-08-09 14:58:20 +12:00
Oscar Zhou
cc7f14951c fix(stack/remote): pass forceRecreate setting [EE-7374] (#12051) 2024-08-06 09:02:21 +12:00
Yajith Dayarathna
b67ff87f35 Installing docker-compose during test-server step (#12075) 2024-08-05 11:28:47 +12:00
andres-portainer
f55ef6e691 fix(pendingactions): remove excessive logging BE-11094 (#12071) 2024-08-02 16:35:14 -03:00
andres-portainer
560a1a00ca fix(scheduler): remove jobs that won't be used anymore BE-11045 (#12058) 2024-08-01 10:59:29 -03:00
andres-portainer
3b5ce1b053 fix(scheduler): remove unnecessary goroutines BE-11044 (#12059) 2024-08-01 10:58:53 -03:00
andres-portainer
03e8d05f18 fix(scheduler): fix a data race in a unit test BE-11084 (#12057) 2024-08-01 10:58:08 -03:00
Oscar Zhou
bedb7fb255 fix(swarm): auto multi-select volume with same name [EE-7240] (#11955) 2024-07-31 12:12:26 +12:00
Oscar Zhou
4d586f7a85 fix(docker): missing browse volume option [EE-7179] (#11901) 2024-07-30 08:53:17 +12:00
Oscar Zhou
6486a5d971 fix(edgestack): broken parallel setting in create view [EE-7245] (#11945) 2024-07-29 09:42:05 +12:00
andres-portainer
e3364457c4 fix(security): update github.com/containers/image/v5 to fix GO-2024-2842 BE-11055 (#12046) 2024-07-23 18:56:17 -03:00
andres-portainer
66119a8b57 fix(snapshots): remove the attempt to snapshot untrusted environments EE-7407 (#12044) 2024-07-23 18:43:31 -03:00
Ali
6eb9e906af fix(placements) filter out empty items in the required node affinity array [BE-11022] (#12034)
Co-authored-by: testa113 <testa113>
2024-07-23 09:31:08 +12:00
LP B
1900fb695d fix(docker/container): use nodeName to build links to networks used by containers (#12002) 2024-07-17 14:40:05 +02:00
Oscar Zhou
a62aac296b fix(host): show clear host info message [EE-7075] (#12011) 2024-07-12 08:45:50 +12:00
Oscar Zhou
5294aa2810 fix(stack): excessive alias count error [EE-7305] (#11990) 2024-07-11 14:09:25 +12:00
andres-portainer
31bdb948a8 chore(code): use int ranges in loops BE-10990 (#12028) 2024-07-10 19:22:47 -03:00
andres-portainer
468c12c75b chore(bbolt): update to v1.3.10 EE-7298 (#12006) 2024-07-09 15:19:27 -03:00
andres-portainer
220fe28830 fix(snapshots): fix background snapshots on environment creation EE-7273 (#12021) 2024-07-09 15:18:13 -03:00
andres-portainer
7fd1a644a6 chore(loops): remove loop var copy EE-7342 (#12023) 2024-07-09 12:47:34 -03:00
andres-portainer
6e7a42727a chore(kompose): remove the code EE-4917 (#12003) 2024-07-08 17:19:07 -03:00
LP B
ac4b129195 fix(docker/network): send target nodeName when removing a network on swarm (#12001) 2024-07-08 17:31:18 +02:00
Steven Kang
85bc14e470 fix(cve): remediate cves detected in docker scout (#12018) 2024-07-08 10:24:39 +12:00
Yajith Dayarathna
6e791a2cfe (fix)nightly code security scan (#12017) 2024-07-06 10:54:41 +12:00
andres-portainer
340830d121 chore(docker): clean up the code EE-7325 (#11997) 2024-07-05 16:19:46 -03:00
andres-portainer
faca64442f chore(code): use cmp.Or() EE-7333 (#12009) 2024-07-04 19:23:53 -03:00
andres-portainer
854474478c chore(go): update to Go v1.22.5 EE-7297 (#12005) 2024-07-03 22:29:20 -03:00
andres-portainer
4adce14485 fix(errors): improve error handling EE-4430 (#11987) 2024-06-28 17:35:26 -03:00
andres-portainer
dc62604ed8 chore(code): remove unused third-party code EE-7306 (#11988) 2024-06-28 17:33:53 -03:00
andres-portainer
f0d43f941f chore(code): replace interface{} with any EE-6513 (#11986) 2024-06-28 14:59:28 -03:00
andres-portainer
9c4935286f chore(jsoniter): remove leftover code EE-6702 (#11984) 2024-06-28 09:46:49 -03:00
andres-portainer
e1648425ea chore(nomad): remove Nomad code EE-7234 (#11980) 2024-06-28 08:54:44 -03:00
andres-portainer
19fa40286a chore(fdo): remove FDO code EE-7235 (#11981) 2024-06-28 08:42:16 -03:00
andres-portainer
1a3db327c7 fix(kube): improve error handling EE-7196 (#11976) 2024-06-27 10:45:11 -03:00
andres-portainer
1170004097 fix(kube): improve error handling EE-7199 (#11974) 2024-06-27 10:43:44 -03:00
Ali
d2b0eacbf5 chore(deps): upgrade typescript to 5.5 [EE-7294] (#11970)
Co-authored-by: testa113 <testa113>
2024-06-27 13:54:10 +12:00
andres-portainer
ca9f85a1ff fix(snapshots): enable the background snapshotter EE-7273 (#11973) 2024-06-26 18:27:39 -03:00
andres-portainer
9ee092aa5e chore(code): reduce the code duplication EE-7278 (#11969) 2024-06-26 18:14:22 -03:00
Oscar Zhou
39bdfa4512 fix(edgestack): gitops auto update issue [EE-7260] (#11965) 2024-06-27 08:48:03 +12:00
cmeng
e828615467 fix(host-info) host info improvement EE-7075 (#11884) 2024-06-26 12:18:22 -03:00
Ali
ba4526985a fix(app): show services explanation in app form [EE-7284] (#11967)
Co-authored-by: testa113 <testa113>
2024-06-26 10:44:20 +12:00
Chaim Lev-Ari
607feb183e refactor(ui/button): remove duplicate data-cy [EE-7242] (#11934) 2024-06-20 15:39:03 +02:00
LP B
9994ed157a fix(app): properly update the app state when losing connectivity to a remote environment while browsing it (#11942) 2024-06-19 13:44:57 +02:00
andres-portainer
bfa27d9103 chore(code): clean up the code EE-7251 (#11948) 2024-06-18 15:59:12 -03:00
Ali
be9d3285e1 fix(custom-templates): add stack validation, remove custom template validation [EE-7102] (#11938)
Co-authored-by: testa113 <testa113>
2024-06-17 09:24:54 +12:00
Chaim Lev-Ari
0f5988af49 fix(edge/stacks): load template [EE-7109] (#11848) 2024-06-16 07:54:00 +03:00
Chaim Lev-Ari
a28bd349ae fix(edge/update): show environment count when more than 100 [EE-6424] (#11917) 2024-06-14 18:37:45 -03:00
Chaim Lev-Ari
51f9977885 fix(endpoints): show toaster on delete [EE-7170] (#11889) 2024-06-13 18:32:17 -03:00
Ali
27865981df fix(namespace): sanitize owner label [EE-7122] (#11935)
Co-authored-by: testa113 <testa113>
2024-06-13 11:06:17 +12:00
James Carppe
ac3f1cd5c3 Add support for specifying the NFS server address in the mount point EE-7019 (#11921) 2024-06-12 11:23:08 -03:00
Dakota Walsh
7549b6cf3f fix(kubernetes): cluster setup screen text on own line EE-7112 (#11905) 2024-06-12 08:43:17 +12:00
Oscar Zhou
dd372ee122 fix(customtemplate): duplicated error handling [EE-7197] (#11913) 2024-06-11 22:11:15 +12:00
LP B
6a8e6734f3 feat(app): limit the docker API version supported by the frontend (#11855) 2024-06-10 20:54:31 +02:00
andres-portainer
4ba16f1b04 chore(errors): remove superfluous error handling EE-7192 (#11909) 2024-06-10 09:57:02 -03:00
andres-portainer
90a19cec5c chore(code): remove unnecessary type conversions EE-7191 (#11908) 2024-06-10 09:32:52 -03:00
Chaim Lev-Ari
8e480c9fab fix(ui): add accessibility labels to access control fieldset (#11439) 2024-06-09 14:34:22 +03:00
Chaim Lev-Ari
b0e3afa0b6 feat(edge/stacks): default refresh rate to 10s [EE-7155] (#11891) 2024-06-09 14:17:21 +03:00
Chaim Lev-Ari
eb6d251a73 feat(edge/jobs): migrate item view to react [EE-2220] (#11887) 2024-06-06 21:07:39 +03:00
Matt Hook
62c2bf86aa fix(db): fix missing portainer.edb in backups when encrypted portainer db is used [EE-6417] (#11885) 2024-06-06 12:36:27 +12:00
Oscar Zhou
4a7f96caf6 fix(stack): unable to delete invalid stack [EE-5753] (#11813) 2024-06-04 11:34:02 +12:00
Chaim Lev-Ari
9c70a43ac3 refactor(edge/groups): migrate view to react [EE-2219] (#11758) 2024-06-02 15:43:37 +03:00
Chaim Lev-Ari
b7cde35c3d fix(ui/datatables): make empty table label consistent [EE-6499] (#11612) 2024-06-02 12:29:20 +03:00
Chaim Lev-Ari
02fbdfec36 feat(edge/jobs): migrate create view to react [EE-2221] (#11867) 2024-06-02 11:10:38 +03:00
Chaim Lev-Ari
94c91035a7 refactor(custom-templates): migrate list view to react [EE-2256] (#11611) 2024-05-30 12:04:28 +03:00
Matt Hook
5c6c66f010 ix(pendingactions): fix deadlock and reduce needless debug logging [EE-7049] (#11869) 2024-05-30 14:55:16 +12:00
Oscar Zhou
0c870bf37b fix(compose): add project directory option to compose command [EE-7093] (#11870) 2024-05-30 08:47:07 +12:00
matias-portainer
9e0e0a12fa fix(waiting-room): add support for bulk deletion in waiting room EE-7136 (#11879) 2024-05-28 17:18:23 -03:00
andres-portainer
c5a1d7e051 fix(tunnels): make the tunnels more robust EE-7042 (#11877) 2024-05-28 16:42:56 -03:00
andres-portainer
aaab2fa9d8 fix(tls): add support for more cipher suites EE-7150 (#11874) 2024-05-28 15:49:31 -03:00
andres-portainer
ef4beef2ea task(endpoints): change the definition of /endpoints/remove EE-7126 (#11873) 2024-05-28 09:05:35 -03:00
Chaim Lev-Ari
1261887c9e fix(stacks): store filter state [EE-5159] (#11637) 2024-05-28 08:14:12 +03:00
cmeng
84fe3cf2a2 fix(stack): remove tailing slash of git url EE-6664 (#11773) 2024-05-28 09:24:29 +12:00
Chaim Lev-Ari
50fd7c6286 feat(docker/containers): limit items on volume selector [EE-7077] (#11845) 2024-05-23 13:15:36 +03:00
cmeng
d7b412eccc fix(container): replace container using correct node name EE-7066 (#11847) 2024-05-23 09:13:49 +12:00
Oscar Zhou
d283c63a33 fix(api/docker): no authorized user can call restricted api [EE-6808] (#11480) 2024-05-22 09:09:06 +12:00
James Carppe
d15e2cdc0c Update bug report template for 2.20.3 (#11846) 2024-05-21 12:50:29 +12:00
Matt Hook
9cef912c44 feat(dashboard): dashboard api [EE-7111] (#11843) 2024-05-21 11:09:29 +12:00
Oscar Zhou
659abe553d fix(edge/stack): edge stack env table pagination and action [EE-6836] (#11837) 2024-05-21 09:40:11 +12:00
Chaim Lev-Ari
014a590704 refactor(docker): migrate dashboard to react [EE-2191] (#11574) 2024-05-20 09:34:51 +03:00
cmeng
2669a44d79 fix(react-query): set react-query networkMode to offlineFirst EE-7081 (#11812) 2024-05-20 15:29:56 +12:00
Matt Hook
db8f9c6f6c fix(console): fix command not found [EE-6982] (#11825) 2024-05-20 14:35:29 +12:00
andres-portainer
2b01136d03 feat(demo): remove demo mode EE-6769 (#11841) 2024-05-17 20:00:01 -03:00
andres-portainer
fbbf550730 fix(endpoints): remove all the endpoints in the same transaction EE-7095 (#11839) 2024-05-17 16:45:06 -03:00
cmeng
3924d0f081 fix(deletion): delete objects batch by batch EE-7084 (#11833) 2024-05-16 14:34:50 +12:00
Matt Hook
00ab9e949a fix(pending-actions): correctly detect unreachable/down cluster [EE-7049] (#11809) 2024-05-16 09:03:10 +12:00
Chaim Lev-Ari
42d9dfba36 fix(docker/volumes): return 409 on volume conflict [EE-6748] (#11691) 2024-05-15 08:27:44 +03:00
Chaim Lev-Ari
a808f83e7d fix(ui): use expand button in sidebar and tables [EE-6844] (#11608) 2024-05-15 08:26:23 +03:00
Matt Hook
413b9c3b04 fix(terminal): don't close terminal on websocket close [EE-6631] (#11824) 2024-05-15 16:17:32 +12:00
Matt Hook
7edce528d6 fix(console): remove deprecated httputil and update console [EE-6468] (#10848) 2024-05-15 10:28:21 +12:00
Chaim Lev-Ari
836df78181 fix(templates): remove console.log [EE-7092] (#11815) 2024-05-14 09:11:05 +03:00
Ali
a80aa2b45c fix(app): ensure placement errors surface per node [EE-7065] (#11820)
Co-authored-by: testa113 <testa113>
2024-05-14 13:39:53 +12:00
Ali
9dd9ffdb3b fix(app): redirect to app after edit [EE-6385] (#11772)
Co-authored-by: testa113 <testa113>
2024-05-14 13:34:28 +12:00
Ali
b6daee2850 fix(app): surface placement rules from form [EE-6553] (#11816) 2024-05-14 13:34:06 +12:00
Ali
1ba4b590f4 fix(app): statefulset pvc summary [EE-6760] (#11802) 2024-05-14 13:33:25 +12:00
Ali
e73b1aa49c fix(docker): log cleanup errors during endpointforceupdate [EE-7055] (#11762) 2024-05-13 15:34:13 +12:00
Ali
6b5a402962 fix(errors): surface react docker errors to front end [EE-7053] (#11726)
Co-authored-by: testa113 <testa113>
2024-05-13 15:34:00 +12:00
Ali
55667a878a fix(gitops): manifest validation warning [EE-6859] (#11664) 2024-05-13 15:09:25 +12:00
Ali
a0ab82b866 fix(LDAP): skip pw validation on edit [EE-616] (#11666)
Co-authored-by: testa113 <testa113>
2024-05-13 15:08:48 +12:00
Matt Hook
6a51b6b41e fix(pending-actions): further refactoring [EE-7011] (#11806) 2024-05-10 11:59:58 +12:00
matias-portainer
b4e829e8c6 fix(waiting-room): add icon in list title EE-6687 (#11092) 2024-05-09 19:24:04 -03:00
Oscar Zhou
06ef12d0ff fix(image): github registry image truncated [EE-7021] (#11769) 2024-05-10 09:01:54 +12:00
Chaim Lev-Ari
cd5f342da0 refactor(edge/stacks): migrate edit view to react [EE-2222] (#11648) 2024-05-09 18:02:20 +03:00
Oscar Zhou
27e309754e fix(api): list docker volume performance [EE-6896] (#11541) 2024-05-09 13:02:56 +12:00
Ali
6ae0a972d4 fix(docker): surface node details docker error [EE-7054] (#11752)
Co-authored-by: testa113 <testa113>
2024-05-09 12:01:13 +12:00
Dakota Walsh
014c491205 fix(sidebar): environment names on hover EE-6854 (#11755) 2024-05-08 17:08:07 -04:00
Dakota Walsh
4ef71f4aca fix(account): enable add access token button EE-7059 (#11745) 2024-05-08 17:07:44 -04:00
Matt Hook
5a5a10821d fix(pendingactions): refactor pending actions [EE-7011] (#11780) 2024-05-09 08:10:10 +12:00
cmeng
9685e260ea fix(docker): keep /docker url prefix for DockerHandler EE-7073 (#11801) 2024-05-08 14:26:53 +12:00
Ali
f8871fcd2a fix(auth logs): fix typo in search keyword [EE-6742] (#11790)
Co-authored-by: testa113 <testa113>
2024-05-08 09:15:56 +12:00
Ali
6d17d8bc64 fix(be-overlay): consistency overlay with variants [EE-6742] (#11774)
Co-authored-by: testa113 <testa113>
2024-05-07 16:16:49 +12:00
Ali
46c6a0700f fix(app): show one tooltip to describe rollback feature [EE-6825] (#11777)
Co-authored-by: testa113 <testa113>
2024-05-07 15:27:22 +12:00
cmeng
5f8fd99fe8 fix(container): specify node name when get a container EE-6981 (#11748) 2024-05-07 11:34:46 +12:00
Chaim Lev-Ari
8a81d95253 refactor(edge/stacks): migrate create view to react [EE-2223] (#11575) 2024-05-06 08:08:03 +03:00
Prabhat Khera
f22aed34b5 fix(pending-action): pending action data format [EE-7064] (#11766) 2024-05-06 15:46:51 +12:00
Steven Kang
e75e6cb7f7 fix: windows container capability [EE-5814] (#11764) 2024-05-03 10:56:34 +12:00
Ali
14a365045d fix(configs): update unused badge logic [EE-6608] (#11500)
Co-authored-by: testa113 <testa113>
2024-05-03 09:13:33 +12:00
Prabhat Khera
9b6779515e fix(kubernetes): namespace yaml [EE-6701] (#11747) 2024-05-03 09:12:37 +12:00
Matt Hook
88ee1b5d19 fix(kube): correctly extract namespace from namespace manifest [EE-6555] (#11676)
Co-authored-by: Prabhat Khera <prabhat.khera@portainer.io>
2024-05-02 14:28:11 +12:00
Matt Hook
a45ec9a7b4 fix(kube): fix text in activity and authentication logs teasers [EE-6742] (#11683)
Co-authored-by: testa113 <testa113>
2024-05-02 14:23:56 +12:00
Ali
51605c6442 fix(app): explain rollback tooltip [EE-6825] (#11698)
Co-authored-by: testa113 <testa113>
2024-05-02 14:10:36 +12:00
Dakota Walsh
2fe213d864 fix(metadata): add mutli endpoint delete api EE-6872 (#11550) 2024-04-30 21:32:20 -04:00
Dakota Walsh
439f13af19 fix(migration): improper version EE-7048 (#11712) 2024-04-30 21:30:40 -04:00
James Carppe
2b5ecd3a57 Add 2.20.2 to bug report template (#11751) 2024-05-01 12:55:14 +12:00
cmeng
a9ead542b3 fix(edge-stack): add completed status EE-6210 (#11632) 2024-04-30 13:44:08 +12:00
Ali
7479302043 fix(jwt): handle kubeconfig with no expiry [EE-7044] (#11710)
Co-authored-by: testa113 <testa113>
2024-04-30 09:22:45 +12:00
Ali
10d20e5963 fix(version): reduce github requests [EE-7017] (#11677) 2024-04-26 08:46:02 +12:00
Ali
5a2e6d0e50 fix(app): avoid 'no label' error when deleting external app [EE-6019] (#11671) 2024-04-26 08:42:10 +12:00
andres-portainer
9068cfd892 chore(code): remove superfluous checks EE-7040 (#11692) 2024-04-25 11:25:23 -03:00
Chaim Lev-Ari
5560a444e5 fix(users): return json from create token [EE-6856] (#11577) 2024-04-25 10:10:42 +03:00
Matt Hook
505a2d5523 fix(jwt): upgrade jwt to remove deprecated jwt.StandardClaims [EE-6469] (#10850) 2024-04-23 17:33:36 +12:00
Ali
2463648161 fix(node): check more node role labels [EE-6968] (#11658)
Co-authored-by: testa113 <testa113>
2024-04-23 16:16:41 +12:00
Ali
48cf27a3b8 fix(migration): run post init migrations for edge after server starts [EE-6905] (#11546)
Co-authored-by: testa113 <testa113>
2024-04-23 16:15:28 +12:00
Matt Hook
39fce3e29b fix(published-ports): fix published port link and into a new component [EE-6592] (#11656) 2024-04-23 13:47:37 +12:00
Matt Hook
4f4c685085 fix(settings): fix crash during settings update when not using oauth [EE-7031] (#11662) 2024-04-23 12:58:28 +12:00
Prabhat Khera
d177a70c54 fix(stack): correct documentation link for stack ENV variables [EE-6902] (#11654) 2024-04-23 08:35:34 +12:00
James Carppe
cf8ec631dd Add 2.19.5 to bug report template (#11652) 2024-04-22 13:44:10 +12:00
Ali
ea61f36e5d fix(app): fix app stuck in loading [EE-7014] (#11651)
Co-authored-by: testa113 <testa113>
2024-04-22 13:11:41 +12:00
Oscar Zhou
ffc66647f8 feat(setting/oauth): add authstyle option [EE-6038] (#11610) 2024-04-22 10:35:19 +12:00
Oscar Zhou
6623475035 fix(stack/git): option to overwrite target path during dir move [EE-6871] (#11628) 2024-04-22 10:34:32 +12:00
cmeng
0dd12a218b fix(docker-client): explicitly set docker client scheme EE-6935 (#11520) 2024-04-22 09:00:45 +12:00
Chaim Lev-Ari
5f89d70fd8 refactor(datatables): remove angular table utilities [EE-4700] (#11634) 2024-04-21 04:47:09 +03:00
Ali
3ccbd40232 fix(stacks): conditionally hide node and namespace stacks [EE-6949] (#11527)
Co-authored-by: testa113 <testa113>
2024-04-19 17:33:22 +12:00
Prabhat Khera
7e9dd01265 fix(swagger): swagger docs for http status code 409 [EE-5767] (#11535) 2024-04-19 15:19:13 +12:00
Matt Hook
0fb3555a70 chore(kubectl): update kubectl to latest point release [EE-7018] (#11620) 2024-04-19 11:46:44 +12:00
andres-portainer
73ce754316 fix(workflows): upgrade Go to v1.21.9 EE-6939 (#11641) 2024-04-18 19:03:13 -03:00
Prabhat Khera
d304f330e8 fix(stack): fix stack env variable link [EE-6902] (#11624) 2024-04-19 07:00:22 +12:00
andres-portainer
7333598dba fix(mingit): upgrade to v2.44.0.1 EE-7023 (#11638) 2024-04-18 15:22:05 -03:00
Ali
bb61e73464 refactor(kube): events datatable react migration [EE-6450] (#11583)
Co-authored-by: testa113 <testa113>
2024-04-18 19:14:09 +12:00
Prabhat Khera
c15789eb73 fix(images): consider stopped containers for unused label [EE-6983] (#11629) 2024-04-18 17:14:39 +12:00
andres-portainer
e7a2b6268e fix(docker): upgrade to v24.0.9 EE-7016 (#11617) 2024-04-17 19:37:57 -03:00
andres-portainer
688fa3aa78 fix(go): upgrade Go to v1.21.9 in the nightly security scan EE-6939 (#11614) 2024-04-17 18:09:53 -03:00
Matt Hook
48bc7d0d92 fix(auth): prevent user enumeration attack [EE-6832] (#11589) 2024-04-17 16:08:27 +12:00
Prabhat Khera
d9df58e93a fix(pending-actions): clean pending actions for deleted environment [EE-6545] (#11598) 2024-04-16 15:09:10 +12:00
Oscar Zhou
37bba18c81 fix(api/endpoint): filter status for async devices [EE-6958] (#11509) 2024-04-16 13:37:04 +12:00
Matt Hook
40498d8ddd chore(docker): bump docker client to 26.0.1 [EE-6941] (#11592) 2024-04-16 08:27:58 +12:00
Prabhat Khera
b265810b95 fix(stacks): update info text for stack environment variables [EE-6902] (#11551) 2024-04-16 08:03:40 +12:00
Prabhat Khera
09837769d7 fix(pending-actions): fix create kubeclient to check endpoint status [EE-6545] (#11584) 2024-04-16 07:40:41 +12:00
Matt Hook
cf1fd17626 chore(api): bump docker and protobuf pkgs [EE-6941] (#11566) 2024-04-15 10:53:15 +12:00
Matt Hook
785f021898 chore(unpacker): use APIVersion as unpacker image tag [EE-6974] (#10955)
Co-authored-by: Prabhat Khera <91852476+prabhat-portainer@users.noreply.github.com>
2024-04-15 10:29:52 +12:00
Prabhat Khera
80cc9f18b5 chore(unpacker): use APIVersion as unpacker image tag [EE-6974] (#11506) 2024-04-15 10:29:24 +12:00
Matt Hook
5e7e91dd6d bump helm version (#11562) 2024-04-15 09:18:04 +12:00
Chaim Lev-Ari
1032b462b4 chore(deps): upgrade react-query to v4 [EE-6638] (#11041) 2024-04-14 17:54:25 +03:00
andres-portainer
104307b2b2 fix(protobuf): upgrade protobuf to v1.33 EE-6945 (#11570) 2024-04-12 17:52:35 -03:00
andres-portainer
f8c66a31d9 fix(go): upgrade Go to v1.21.9 EE-6939 (#11554) 2024-04-12 17:08:07 -03:00
Chaim Lev-Ari
2100155ab5 refactor(docker/containers): migrate inspect view to react [EE-2190] (#11005) 2024-04-11 19:07:58 +03:00
Chaim Lev-Ari
de473fc10e refactor(docker): remove EndpointProvider from exec [EE-6462] (#10840) 2024-04-11 19:04:58 +03:00
2114 changed files with 53504 additions and 38338 deletions

52
.air.toml Normal file
View File

@@ -0,0 +1,52 @@
root = "."
testdata_dir = "testdata"
tmp_dir = ".tmp"
[build]
args_bin = []
bin = "./dist/portainer"
cmd = "SKIP_GO_GET=true make build-server"
delay = 1000
exclude_dir = []
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = "./dist/portainer --log-level=DEBUG"
include_dir = ["api"]
include_ext = ["go"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@@ -87,6 +87,7 @@ overrides:
version: 'detect'
rules:
no-console: error
import/order:
[
'error',

View File

@@ -2,16 +2,17 @@ name: Bug Report
description: Create a report to help us improve.
labels: kind/bug,bug/need-confirmation
body:
- type: markdown
attributes:
value: |
# Welcome!
The issue tracker is for reporting bugs. If you have an [idea for a new feature](https://github.com/orgs/portainer/discussions/categories/ideas) or a [general question about Portainer](https://github.com/orgs/portainer/discussions/categories/help) please post in our [GitHub Discussions](https://github.com/orgs/portainer/discussions).
You can also ask for help in our [community Slack channel](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA).
Please note that we only provide support for current versions of Portainer. You can find a list of supported versions in our [lifecycle policy](https://docs.portainer.io/start/lifecycle).
**DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS**.
- type: checkboxes
@@ -43,7 +44,7 @@ body:
- type: textarea
attributes:
label: Problem Description
description: A clear and concise description of what the bug is.
description: A clear and concise description of what the bug is.
validations:
required: true
@@ -69,7 +70,7 @@ body:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
4. See error
validations:
required: true
@@ -90,25 +91,36 @@ body:
- type: dropdown
attributes:
label: Portainer version
description: We only provide support for the most recent version of Portainer and the previous 3 versions. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
description: We only provide support for current versions of Portainer as per the lifecycle policy linked above. If you are on an older version of Portainer we recommend [upgrading first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
multiple: false
options:
- '2.28.0'
- '2.27.1'
- '2.27.0'
- '2.26.1'
- '2.26.0'
- '2.25.1'
- '2.25.0'
- '2.24.1'
- '2.24.0'
- '2.23.0'
- '2.22.0'
- '2.21.5'
- '2.21.4'
- '2.21.3'
- '2.21.2'
- '2.21.1'
- '2.21.0'
- '2.20.3'
- '2.20.2'
- '2.20.1'
- '2.20.0'
- '2.19.5'
- '2.19.4'
- '2.19.3'
- '2.19.2'
- '2.19.1'
- '2.19.0'
- '2.18.4'
- '2.18.3'
- '2.18.2'
- '2.18.1'
- '2.17.1'
- '2.17.0'
- '2.16.2'
- '2.16.1'
- '2.16.0'
validations:
required: true
@@ -146,7 +158,7 @@ body:
- type: input
attributes:
label: Browser
description: |
description: |
Enter your browser and version. Example: Google Chrome 114.0
validations:
required: false

View File

@@ -1,176 +0,0 @@
name: ci
on:
workflow_dispatch:
push:
branches:
- 'develop'
- 'release/*'
pull_request:
branches:
- 'develop'
- 'release/*'
- 'feat/*'
- 'fix/*'
- 'refactor/*'
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
DOCKER_HUB_REPO: portainerci/portainer-ce
EXTENSION_HUB_REPO: portainerci/portainer-docker-extension
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
jobs:
build_images:
strategy:
matrix:
config:
- { platform: linux, arch: amd64, version: "" }
- { platform: linux, arch: arm64, version: "" }
- { platform: linux, arch: arm, version: "" }
- { platform: linux, arch: ppc64le, version: "" }
- { platform: linux, arch: s390x, version: "" }
- { platform: windows, arch: amd64, version: 1809 }
- { platform: windows, arch: amd64, version: ltsc2022 }
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: '[preparation] checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: '[preparation] set up golang'
uses: actions/setup-go@v5.0.0
with:
go-version: ${{ env.GO_VERSION }}
- name: '[preparation] set up node.js'
uses: actions/setup-node@v4.0.1
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- name: '[preparation] set up qemu'
uses: docker/setup-qemu-action@v3.0.0
- name: '[preparation] set up docker context for buildx'
run: docker context create builders
- name: '[preparation] set up docker buildx'
uses: docker/setup-buildx-action@v3.0.0
with:
endpoint: builders
- name: '[preparation] docker login'
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: '[preparation] set the container image tag'
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
# use the release branch name as the tag for release branches
# for instance, release/2.19 becomes 2.19
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
# use pr${{ github.event.number }} as the tag for pull requests
# for instance, pr123
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
else
# replace / with - in the branch name
# for instance, feature/1.0.0 -> feature-1.0.0
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
fi
echo "CONTAINER_IMAGE_TAG=${CONTAINER_IMAGE_TAG}-${{ matrix.config.platform }}${{ matrix.config.version }}-${{ matrix.config.arch }}" >> $GITHUB_ENV
- name: '[execution] build linux & windows portainer binaries'
run: |
export YARN_VERSION=$(yarn --version)
export WEBPACK_VERSION=$(yarn list webpack --depth=0 | grep webpack | awk -F@ '{print $2}')
export BUILDNUMBER=${GITHUB_RUN_NUMBER}
GIT_COMMIT_HASH_LONG=${{ github.sha }}
export GIT_COMMIT_HASH_SHORT={GIT_COMMIT_HASH_LONG:0:7}
NODE_ENV="testing"
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
NODE_ENV="production"
fi
make build-all PLATFORM=${{ matrix.config.platform }} ARCH=${{ matrix.config.arch }} ENV=${NODE_ENV}
env:
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
- name: '[execution] build and push docker images'
run: |
if [ "${{ matrix.config.platform }}" == "windows" ]; then
mv dist/portainer dist/portainer.exe
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
else
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
fi
fi
env:
CONTAINER_IMAGE_TAG: ${{ env.CONTAINER_IMAGE_TAG }}
build_manifests:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
needs: [build_images]
steps:
- name: '[preparation] docker login'
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: '[preparation] set up docker context for buildx'
run: docker version && docker context create builders
- name: '[preparation] set up docker buildx'
uses: docker/setup-buildx-action@v3.0.0
with:
endpoint: builders
- name: '[execution] build and push manifests'
run: |
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
# use the release branch name as the tag for release branches
# for instance, release/2.19 becomes 2.19
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | cut -d "/" -f 2)
elif [ "${GITHUB_EVENT_NAME}" == "pull_request" ]; then
# use pr${{ github.event.number }} as the tag for pull requests
# for instance, pr123
CONTAINER_IMAGE_TAG="pr${{ github.event.number }}"
else
# replace / with - in the branch name
# for instance, feature/1.0.0 -> feature-1.0.0
CONTAINER_IMAGE_TAG=$(echo $GITHUB_REF_NAME | sed 's/\//-/g')
fi
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windows1809-amd64" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-windowsltsc2022-amd64"
docker buildx imagetools create -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-s390x"
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64-alpine" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm-alpine"
fi

View File

@@ -1,15 +0,0 @@
on:
push:
branches:
- develop
- 'release/**'
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: mschilde/auto-label-merge-conflicts@master
with:
CONFLICT_LABEL_NAME: 'has conflicts'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MAX_RETRIES: 10
WAIT_MS: 60000

View File

@@ -1,55 +0,0 @@
name: Lint
on:
push:
branches:
- master
- develop
- release/*
pull_request:
branches:
- master
- develop
- release/*
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- run: yarn --frozen-lockfile
- name: Run linters
uses: wearerequired/lint-action@v1
with:
eslint: true
eslint_extensions: ts,tsx,js,jsx
prettier: true
prettier_dir: app/
gofmt: true
gofmt_dir: api/
- name: Typecheck
uses: icrawl/action-tsc@v1
- name: GolangCI-Lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.55.2
args: --timeout=10m -c .golangci.yaml

View File

@@ -1,252 +0,0 @@
name: Nightly Code Security Scan
on:
schedule:
- cron: '0 20 * * *'
workflow_dispatch:
env:
GO_VERSION: 1.21.6
jobs:
client-dependencies:
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:
- name: checkout repository
uses: actions/checkout@master
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-develop-result
path: snyk.json
- 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 html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-js-result-${{github.run_id}}
path: js-result.html
- name: analyse vulnerabilities
id: set-matrix
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
runs-on: ubuntu-latest
if: >- # only run for develop branch
github.ref == 'refs/heads/develop'
outputs:
go: ${{ steps.set-matrix.outputs.go_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
yarn global add snyk
snyk test --file=./go.mod --json-file-output=snyk.json 2>/dev/null || :
- name: upload scan result as develop artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-develop-result
path: snyk.json
- 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 html file as artifact
uses: actions/upload-artifact@v3
with:
name: html-go-result-${{github.run_id}}
path: go-result.html
- name: analyse vulnerabilities
id: set-matrix
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: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
outputs:
image-trivy: ${{ steps.set-trivy-matrix.outputs.image_trivy_result }}
image-docker-scout: ${{ steps.set-docker-scout-matrix.outputs.image_docker_scout_result }}
steps:
- 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 portainerci/portainer:develop
- name: upload Trivy image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-develop-result
path: image-trivy.json
- name: develop Trivy 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-trivy-result")
- name: upload html file as Trivy artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-${{github.run_id}}
path: image-trivy-result.html
- name: analyse vulnerabilities from Trivy
id: set-trivy-matrix
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_trivy_result=${result}" >> $GITHUB_OUTPUT
- name: scan vulnerabilities by Docker Scout
uses: docker/scout-action@v1
continue-on-error: true
with:
command: cves
image: portainerci/portainer:develop
sarif-file: image-docker-scout.json
dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: upload Docker Scout image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-develop-result
path: image-docker-scout.json
- name: develop Docker Scout scan report export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=docker-scout --path="/data/image-docker-scout.json" --output-type=table --export --export-filename="/data/image-docker-scout-result")
- name: upload html file as Docker Scout artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-${{github.run_id}}
path: image-docker-scout-result.html
- name: analyse vulnerabilities from Docker Scout
id: set-docker-scout-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest summary --report-type=docker-scout --path="/data/image-docker-scout.json" --output-type=matrix)
echo "image_docker_scout_result=${result}" >> $GITHUB_OUTPUT
result-analysis:
name: Analyse Scan Results
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
strategy:
matrix:
js: ${{fromJson(needs.client-dependencies.outputs.js)}}
go: ${{fromJson(needs.server-dependencies.outputs.go)}}
image-trivy: ${{fromJson(needs.image-vulnerability.outputs.image-trivy)}}
image-docker-scout: ${{fromJson(needs.image-vulnerability.outputs.image-docker-scout)}}
steps:
- name: display the results of js, Go, and image scan
run: |
echo "${{ matrix.js.status }}"
echo "${{ matrix.go.status }}"
echo "${{ matrix.image-trivy.status }}"
echo "${{ matrix.image-docker-scout.status }}"
echo "${{ matrix.js.summary }}"
echo "${{ matrix.go.summary }}"
echo "${{ matrix.image-trivy.summary }}"
echo "${{ matrix.image-docker-scout.summary }}"
- name: send message to Slack
if: >-
matrix.js.status == 'failure' ||
matrix.go.status == 'failure' ||
matrix.image-trivy.status == 'failure' ||
matrix.image-docker-scout.status == 'failure'
uses: slackapi/slack-github-action@v1.23.0
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Code Scanning Result (*${{ github.repository }}*)\n*<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|GitHub Actions Workflow URL>*"
}
}
],
"attachments": [
{
"color": "#FF0000",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*JS dependency check*: *${{ matrix.js.status }}*\n${{ matrix.js.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Go dependency check*: *${{ matrix.go.status }}*\n${{ matrix.go.summary }}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Image Trivy vulnerability check*: *${{ matrix.image-trivy.status }}*\n${{ matrix.image-trivy.summary }}\n"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Image Docker Scout vulnerability check*: *${{ matrix.image-docker-scout.status }}*\n${{ matrix.image-docker-scout.summary }}\n"
}
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

View File

@@ -1,298 +0,0 @@
name: PR Code Security Scan
on:
pull_request_review:
types:
- submitted
- edited
paths:
- 'package.json'
- 'go.mod'
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
- '.github/workflows/pr-security.yml'
env:
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
jobs:
client-dependencies:
name: Client Dependency Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
outputs:
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: scan vulnerabilities by Snyk
uses: snyk/actions/node@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
json: true
- 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 built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./js-snyk-feature.json
(gh run download -n js-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./js-snyk-develop.json
else
echo "null" > ./js-snyk-develop.json
fi
- 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 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 different vulnerabilities against develop branch
id: set-diff-matrix
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
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
outputs:
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
steps:
- name: checkout repository
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: download Go modules
run: cd ./api && go get -t -v -d ./...
- name: scan vulnerabilities by Snyk
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: |
yarn global add snyk
snyk test --file=./go.mod --json-file-output=snyk.json 2>/dev/null || :
- 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 built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./snyk.json ./go-snyk-feature.json
(gh run download -n go-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./snyk.json ]]; then
mv ./snyk.json ./go-snyk-develop.json
else
echo "null" > ./go-snyk-develop.json
fi
- 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 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 different vulnerabilities against develop branch
id: set-diff-matrix
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: Image Vulnerability Check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
outputs:
imagediff-trivy: ${{ steps.set-diff-trivy-matrix.outputs.image_diff_trivy_result }}
imagediff-docker-scout: ${{ steps.set-diff-docker-scout-matrix.outputs.image_diff_docker_scout_result }}
steps:
- name: checkout code
uses: actions/checkout@master
- name: install Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: install Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install packages
run: yarn --frozen-lockfile
- name: build
run: make build-all
- name: set up docker buildx
uses: docker/setup-buildx-action@v2
- name: build and compress image
uses: docker/build-push-action@v4
with:
context: .
file: build/linux/Dockerfile
tags: local-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/local-portainer-image.tar
- name: load docker image
run: |
docker load --input /tmp/local-portainer-image.tar
- 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 local-portainer:${{ github.sha }}
- name: upload Trivy image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-feature-result
path: image-trivy.json
- name: download Trivy artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./image-trivy.json ./image-trivy-feature.json
(gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./image-trivy.json ]]; then
mv ./image-trivy.json ./image-trivy-develop.json
else
echo "null" > ./image-trivy-develop.json
fi
- name: pr vs develop Trivy 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-trivy-result")
- name: upload html file as Trivy artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-compare-to-develop-${{github.run_id}}
path: image-trivy-result.html
- name: analyse different vulnerabilities against develop branch by Trivy
id: set-diff-trivy-matrix
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_trivy_result=${result}" >> $GITHUB_OUTPUT
- name: scan vulnerabilities by Docker Scout
uses: docker/scout-action@v1
continue-on-error: true
with:
command: cves
image: local-portainer:${{ github.sha }}
sarif-file: image-docker-scout.json
dockerhub-user: ${{ secrets.DOCKER_HUB_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: upload Docker Scout image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-feature-result
path: image-docker-scout.json
- name: download Docker Scout artifacts from develop branch built by nightly scan
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
mv ./image-docker-scout.json ./image-docker-scout-feature.json
(gh run download -n image-security-scan-develop-result -R ${{ github.repository }} 2>&1 >/dev/null) || :
if [[ -e ./image-docker-scout.json ]]; then
mv ./image-docker-scout.json ./image-docker-scout-develop.json
else
echo "null" > ./image-docker-scout-develop.json
fi
- name: pr vs develop Docker Scout scan report comparison export to html
run: |
$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=docker-scout --path="/data/image-docker-scout-feature.json" --compare-to="/data/image-docker-scout-develop.json" --output-type=table --export --export-filename="/data/image-docker-scout-result")
- name: upload html file as Docker Scout artifact
uses: actions/upload-artifact@v3
with:
name: html-image-result-compare-to-develop-${{github.run_id}}
path: image-docker-scout-result.html
- name: analyse different vulnerabilities against develop branch by Docker Scout
id: set-diff-docker-scout-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data portainerci/code-security-report:latest diff --report-type=docker-scout --path="/data/image-docker-scout-feature.json" --compare-to="/data/image-docker-scout-develop.json" --output-type=matrix)
echo "image_diff_docker_scout_result=${result}" >> $GITHUB_OUTPUT
result-analysis:
name: Analyse Scan Result Against develop Branch
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan' &&
github.event.pull_request.draft == false
strategy:
matrix:
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
imagediff-trivy: ${{fromJson(needs.image-vulnerability.outputs.imagediff-trivy)}}
imagediff-docker-scout: ${{fromJson(needs.image-vulnerability.outputs.imagediff-docker-scout)}}
steps:
- name: check job status of diff result
if: >-
matrix.jsdiff.status == 'failure' ||
matrix.godiff.status == 'failure' ||
matrix.imagediff-trivy.status == 'failure' ||
matrix.imagediff-docker-scout.status == 'failure'
run: |
echo "${{ matrix.jsdiff.status }}"
echo "${{ matrix.godiff.status }}"
echo "${{ matrix.imagediff-trivy.status }}"
echo "${{ matrix.imagediff-docker-scout.status }}"
echo "${{ matrix.jsdiff.summary }}"
echo "${{ matrix.godiff.summary }}"
echo "${{ matrix.imagediff-trivy.summary }}"
echo "${{ matrix.imagediff-docker-scout.summary }}"
exit 1

View File

@@ -1,19 +0,0 @@
name: Automatic Rebase
on:
issue_comment:
types: [created]
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0 # otherwise, you will fail to push refs to dest repo
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,28 +0,0 @@
name: Close Stale Issues
on:
schedule:
- cron: '0 12 * * *'
workflow_dispatch:
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Issue Config
days-before-issue-stale: 60
days-before-issue-close: 7
stale-issue-label: 'status/stale'
exempt-all-issue-milestones: true # Do not stale issues in a milestone
exempt-issue-labels: kind/enhancement, kind/style, kind/workaround, kind/refactor, bug/need-confirmation, bug/confirmed, status/discuss
stale-issue-message: 'This issue has been marked as stale as it has not had recent activity, it will be closed if no further activity occurs in the next 7 days. If you believe that it has been incorrectly labelled as stale, leave a comment and the label will be removed.'
close-issue-message: 'Since no further activity has appeared on this issue it will be closed. If you believe that it has been incorrectly closed, leave a comment mentioning `portainer/support` and one of our staff will then review the issue. Note - If it is an old bug report, make sure that it is reproduceable in the latest version of Portainer as it may have already been fixed.'
# Pull Request Config
days-before-pr-stale: -1 # Do not stale pull request
days-before-pr-close: -1 # Do not close pull request

View File

@@ -1,56 +0,0 @@
name: Test
env:
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
on:
pull_request:
branches:
- master
- develop
- release/*
types:
- opened
- reopened
- synchronize
- ready_for_review
push:
branches:
- master
- develop
- release/*
jobs:
test-client:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: make test-client ARGS="--maxWorkers=2 --minWorkers=1"
test-server:
strategy:
matrix:
config:
- { platform: linux, arch: amd64 }
- { platform: linux, arch: arm64 }
- { platform: windows, arch: amd64, version: 1809 }
- { platform: windows, arch: amd64, version: ltsc2022 }
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Run tests
run: make test-server

View File

@@ -1,39 +0,0 @@
name: Validate OpenAPI specs
on:
pull_request:
branches:
- master
- develop
- 'release/*'
types:
- opened
- reopened
- synchronize
- ready_for_review
env:
GO_VERSION: 1.21.6
NODE_VERSION: 18.x
jobs:
openapi-spec:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Download golang modules
run: cd ./api && go get -t -v -d ./...
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Validate OpenAPI Spec
run: make docs-validate

1
.godir
View File

@@ -1 +0,0 @@
portainer

View File

@@ -9,7 +9,9 @@ linters:
- gosimple
- govet
- errorlint
- exportloopref
- copyloopvar
- intrange
- perfsprint
linters-settings:
depguard:
@@ -18,8 +20,6 @@ linters-settings:
deny:
- pkg: 'encoding/json'
desc: 'use github.com/segmentio/encoding/json'
- pkg: 'github.com/sirupsen/logrus'
desc: 'logging is allowed only by github.com/rs/zerolog'
- pkg: 'golang.org/x/exp'
desc: 'exp is not allowed'
- pkg: 'github.com/portainer/libcrypto'

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged
cd $(dirname -- "$0") && yarn lint-staged

View File

@@ -3,12 +3,11 @@ import React from 'react';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswLoader } from 'msw-storybook-addon';
import { handlers } from '../app/setup-tests/server-handlers';
import { QueryClient, QueryClientProvider } from 'react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
initMSW(
{
onUnhandledRequest: ({ method, url }) => {
console.log(method, url);
if (url.startsWith('/api')) {
console.error(`Unhandled ${method} request to ${url}.

View File

@@ -1,19 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/api/cmd/portainer",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,
"args": ["--data", "${env:HOME}/portainer-data", "--assets", "${workspaceRoot}/dist"]
}
]
}

View File

@@ -1,191 +0,0 @@
{
// Place your portainer workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"React Named Export Component": {
"prefix": "rnec",
"body": [
"export function $TM_FILENAME_BASE() {",
" return <div>$TM_FILENAME_BASE</div>;",
"}"
],
"description": "React Named Export Component"
},
"Component": {
"scope": "javascript",
"prefix": "mycomponent",
"description": "Dummy Angularjs Component",
"body": [
"import angular from 'angular';",
"import controller from './${TM_FILENAME_BASE}Controller'",
"",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').component('$TM_FILENAME_BASE', {",
" templateUrl: './$TM_FILENAME_BASE.html',",
" controller,",
"});",
""
]
},
"Controller": {
"scope": "javascript",
"prefix": "mycontroller",
"body": [
"class ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/} {",
"\t/* @ngInject */",
"\tconstructor($0) {",
"\t}",
"}",
"",
"export default ${TM_FILENAME_BASE/(.*)/${1:/capitalize}/};"
],
"description": "Dummy ES6+ controller"
},
"Service": {
"scope": "javascript",
"prefix": "myservice",
"description": "Dummy ES6+ service",
"body": [
"import angular from 'angular';",
"import PortainerError from 'Portainer/error';",
"",
"class $1 {",
" /* @ngInject */",
" constructor(\\$async, $0) {",
" this.\\$async = \\$async;",
"",
" this.getAsync = this.getAsync.bind(this);",
" this.getAllAsync = this.getAllAsync.bind(this);",
" this.createAsync = this.createAsync.bind(this);",
" this.updateAsync = this.updateAsync.bind(this);",
" this.deleteAsync = this.deleteAsync.bind(this);",
" }",
"",
" /**",
" * GET",
" */",
" async getAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" async getAllAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" get() {",
" if () {",
" return this.\\$async(this.getAsync);",
" }",
" return this.\\$async(this.getAllAsync);",
" }",
"",
" /**",
" * CREATE",
" */",
" async createAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" create() {",
" return this.\\$async(this.createAsync);",
" }",
"",
" /**",
" * UPDATE",
" */",
" async updateAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" update() {",
" return this.\\$async(this.updateAsync);",
" }",
"",
" /**",
" * DELETE",
" */",
" async deleteAsync() {",
" try {",
"",
" } catch (err) {",
" throw new PortainerError('', err);",
" }",
" }",
"",
" delete() {",
" return this.\\$async(this.deleteAsync);",
" }",
"}",
"",
"export default $1;",
"angular.module('portainer.${TM_DIRECTORY/.*\\/app\\/([^\\/]*)(\\/.*)?$/$1/}').service('$1', $1);"
]
},
"swagger-api-doc": {
"prefix": "swapi",
"scope": "go",
"description": "Snippet for a api doc",
"body": [
"// @id ",
"// @summary ",
"// @description ",
"// @description **Access policy**: ",
"// @tags ",
"// @security ApiKeyAuth",
"// @security jwt",
"// @accept json",
"// @produce json",
"// @param id path int true \"identifier\"",
"// @param body body Object true \"details\"",
"// @success 200 {object} portainer. \"Success\"",
"// @success 204 \"Success\"",
"// @failure 400 \"Invalid request\"",
"// @failure 403 \"Permission denied\"",
"// @failure 404 \" not found\"",
"// @failure 500 \"Server error\"",
"// @router /{id} [get]"
]
},
"analytics": {
"prefix": "nlt",
"body": ["analytics-on", "analytics-category=\"$1\"", "analytics-event=\"$2\""],
"description": "analytics"
},
"analytics-if": {
"prefix": "nltf",
"body": ["analytics-if=\"$1\""],
"description": "analytics"
},
"analytics-metadata": {
"prefix": "nltm",
"body": "analytics-properties=\"{ metadata: { $1 } }\""
}
}

View File

@@ -1,8 +0,0 @@
{
"go.lintTool": "golangci-lint",
"go.lintFlags": ["--fast", "-E", "exportloopref"],
"gopls": {
"build.expandWorkspaceToModule": false
},
"gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".git-blame-ignore-revs"]
}

View File

@@ -9,7 +9,7 @@ ENV=development
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
TAG=local
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
GOTESTSUM=go run gotest.tools/gotestsum@latest
# Don't change anything below this line unless you know what you're doing
@@ -17,11 +17,13 @@ GOTESTSUM=go run gotest.tools/gotestsum@latest
##@ Building
.PHONY: init-dist build-storybook build build-client build-server build-image devops
.PHONY: all init-dist build-storybook build build-client build-server build-image devops
init-dist:
@mkdir -p dist
build-all: deps build-server build-client ## Build the client, server and download external dependancies (doesn't build an image)
all: tidy deps build-server build-client ## Build the client, server and download external dependancies (doesn't build an image)
build-all: all ## Alias for the 'all' target (used by CI)
build-client: init-dist ## Build the client
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
@@ -30,7 +32,7 @@ build-server: init-dist ## Build the server binary
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
build-image: build-all ## Build the Portainer image locally
docker buildx build --load -t portainerci/portainer:$(TAG) -f build/linux/Dockerfile .
docker buildx build --load -t portainerci/portainer-ce:$(TAG) -f build/linux/Dockerfile .
build-storybook: ## Build and serve the storybook files
yarn storybook:build
@@ -50,7 +52,7 @@ client-deps: ## Install client dependencies
yarn
tidy: ## Tidy up the go.mod file
cd api && go mod tidy
@go mod tidy
##@ Cleanup
@@ -65,23 +67,25 @@ clean: ## Remove all build and download artifacts
test: test-server test-client ## Run all tests
test-client: ## Run client tests
yarn test $(ARGS)
yarn test $(ARGS) --coverage
test-server: ## Run server tests
$(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover ./...
$(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover -covermode=atomic -coverprofile=coverage.out ./...
##@ Dev
.PHONY: dev dev-client dev-server
dev: ## Run both the client and server in development mode
dev: ## Run both the client and server in development mode
make dev-server
make dev-client
dev-client: ## Run the client in development mode
dev-client: ## Run the client in development mode
yarn dev
dev-server: build-server ## Run the server in development mode
@./dev/run_container.sh
dev-server-podman: build-server ## Run the server in development mode
@./dev/run_container_podman.sh
##@ Format
.PHONY: format format-client format-server
@@ -114,7 +118,7 @@ dev-extension: build-server build-client ## Run the extension in development mod
##@ Docs
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
docs-build: init-dist ## Build docs
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./
docs-validate: docs-build ## Validate docs
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml

View File

@@ -10,7 +10,7 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/url"
"github.com/portainer/portainer/api/url"
)
// GetAgentVersionAndPlatform returns the agent version and platform

View File

@@ -3,7 +3,6 @@ package apikey
import (
"testing"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/stretchr/testify/assert"
)
@@ -34,17 +33,19 @@ func Test_generateRandomKey(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := securecookie.GenerateRandomKey(tt.wantLenth)
got := GenerateRandomKey(tt.wantLenth)
is.Equal(tt.wantLenth, len(got))
})
}
t.Run("Generated keys are unique", func(t *testing.T) {
keys := make(map[string]bool)
for i := 0; i < 100; i++ {
key := securecookie.GenerateRandomKey(8)
for range 100 {
key := GenerateRandomKey(8)
_, ok := keys[string(key)]
is.False(ok)
keys[string(key)] = true
}
})

View File

@@ -1,69 +1,79 @@
package apikey
import (
lru "github.com/hashicorp/golang-lru"
portainer "github.com/portainer/portainer/api"
lru "github.com/hashicorp/golang-lru"
)
const defaultAPIKeyCacheSize = 1024
const DefaultAPIKeyCacheSize = 1024
// entry is a tuple containing the user and API key associated to an API key digest
type entry struct {
user portainer.User
type entry[T any] struct {
user T
apiKey portainer.APIKey
}
// apiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
type UserCompareFn[T any] func(T, portainer.UserID) bool
// ApiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
// We store the api-key digest (keys) and the associated user and key-data (values) in the cache.
// This is required because HTTP requests will contain only the api-key digest in the x-api-key request header;
// digest value must be mapped to a portainer user (and respective key data) for validation.
// This cache is used to avoid multiple database queries to retrieve these user/key associated to the digest.
type apiKeyCache struct {
type ApiKeyCache[T any] struct {
// cache type [string]entry cache (key: string(digest), value: user/key entry)
// note: []byte keys are not supported by golang-lru Cache
cache *lru.Cache
cache *lru.Cache
userCmpFn UserCompareFn[T]
}
// NewAPIKeyCache creates a new cache for API keys
func NewAPIKeyCache(cacheSize int) *apiKeyCache {
func NewAPIKeyCache[T any](cacheSize int, userCompareFn UserCompareFn[T]) *ApiKeyCache[T] {
cache, _ := lru.New(cacheSize)
return &apiKeyCache{cache: cache}
return &ApiKeyCache[T]{cache: cache, userCmpFn: userCompareFn}
}
// Get returns the user/key associated to an api-key's digest
// This is required because HTTP requests will contain the digest of the API key in header,
// the digest value must be mapped to a portainer user.
func (c *apiKeyCache) Get(digest string) (portainer.User, portainer.APIKey, bool) {
func (c *ApiKeyCache[T]) Get(digest string) (T, portainer.APIKey, bool) {
val, ok := c.cache.Get(digest)
if !ok {
return portainer.User{}, portainer.APIKey{}, false
var t T
return t, portainer.APIKey{}, false
}
tuple := val.(entry)
tuple := val.(entry[T])
return tuple.user, tuple.apiKey, true
}
// Set persists a user/key entry to the cache
func (c *apiKeyCache) Set(digest string, user portainer.User, apiKey portainer.APIKey) {
c.cache.Add(digest, entry{
func (c *ApiKeyCache[T]) Set(digest string, user T, apiKey portainer.APIKey) {
c.cache.Add(digest, entry[T]{
user: user,
apiKey: apiKey,
})
}
// Delete evicts a digest's user/key entry key from the cache
func (c *apiKeyCache) Delete(digest string) {
func (c *ApiKeyCache[T]) Delete(digest string) {
c.cache.Remove(digest)
}
// InvalidateUserKeyCache loops through all the api-keys associated to a user and removes them from the cache
func (c *apiKeyCache) InvalidateUserKeyCache(userId portainer.UserID) bool {
func (c *ApiKeyCache[T]) InvalidateUserKeyCache(userId portainer.UserID) bool {
present := false
for _, k := range c.cache.Keys() {
user, _, _ := c.Get(k.(string))
if user.ID == userId {
if c.userCmpFn(user, userId) {
present = c.cache.Remove(k)
}
}
return present
}

View File

@@ -10,11 +10,11 @@ import (
func Test_apiKeyCacheGet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
keyCache := NewAPIKeyCache(10, compareUser)
// pre-populate cache
keyCache.cache.Add(string("foo"), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string(""), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string(""), entry[portainer.User]{user: portainer.User{}, apiKey: portainer.APIKey{}})
tests := []struct {
digest string
@@ -35,7 +35,7 @@ func Test_apiKeyCacheGet(t *testing.T) {
}
for _, test := range tests {
t.Run(string(test.digest), func(t *testing.T) {
t.Run(test.digest, func(t *testing.T) {
_, _, found := keyCache.Get(test.digest)
is.Equal(test.found, found)
})
@@ -45,7 +45,7 @@ func Test_apiKeyCacheGet(t *testing.T) {
func Test_apiKeyCacheSet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
keyCache := NewAPIKeyCache(10, compareUser)
// pre-populate cache
keyCache.Set("bar", portainer.User{ID: 2}, portainer.APIKey{})
@@ -57,23 +57,23 @@ func Test_apiKeyCacheSet(t *testing.T) {
val, ok := keyCache.cache.Get(string("bar"))
is.True(ok)
tuple := val.(entry)
tuple := val.(entry[portainer.User])
is.Equal(portainer.User{ID: 2}, tuple.user)
val, ok = keyCache.cache.Get(string("foo"))
is.True(ok)
tuple = val.(entry)
tuple = val.(entry[portainer.User])
is.Equal(portainer.User{ID: 3}, tuple.user)
}
func Test_apiKeyCacheDelete(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
keyCache := NewAPIKeyCache(10, compareUser)
t.Run("Delete an existing entry", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.Delete("foo")
_, ok := keyCache.cache.Get(string("foo"))
@@ -128,7 +128,7 @@ func Test_apiKeyCacheLRU(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
keyCache := NewAPIKeyCache(test.cacheLen)
keyCache := NewAPIKeyCache(test.cacheLen, compareUser)
for _, key := range test.key {
keyCache.Set(key, portainer.User{ID: 1}, portainer.APIKey{})
@@ -150,10 +150,10 @@ func Test_apiKeyCacheLRU(t *testing.T) {
func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
keyCache := NewAPIKeyCache(10, compareUser)
t.Run("Removes users keys from cache", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
@@ -163,8 +163,8 @@ func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
})
t.Run("Does not affect other keys", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("bar"), entry{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("foo"), entry[portainer.User]{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("bar"), entry[portainer.User]{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)

View File

@@ -1,14 +1,15 @@
package apikey
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/securecookie"
"github.com/pkg/errors"
)
@@ -20,30 +21,45 @@ var ErrInvalidAPIKey = errors.New("Invalid API key")
type apiKeyService struct {
apiKeyRepository dataservices.APIKeyRepository
userRepository dataservices.UserService
cache *apiKeyCache
cache *ApiKeyCache[portainer.User]
}
// GenerateRandomKey generates a random key of specified length
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
func GenerateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}
func compareUser(u portainer.User, id portainer.UserID) bool {
return u.ID == id
}
func NewAPIKeyService(apiKeyRepository dataservices.APIKeyRepository, userRepository dataservices.UserService) *apiKeyService {
return &apiKeyService{
apiKeyRepository: apiKeyRepository,
userRepository: userRepository,
cache: NewAPIKeyCache(defaultAPIKeyCacheSize),
cache: NewAPIKeyCache(DefaultAPIKeyCacheSize, compareUser),
}
}
// HashRaw computes a hash digest of provided raw API key.
func (a *apiKeyService) HashRaw(rawKey string) string {
hashDigest := sha256.Sum256([]byte(rawKey))
return base64.StdEncoding.EncodeToString(hashDigest[:])
}
// GenerateApiKey generates a raw API key for a user (for one-time display).
// The generated API key is stored in the cache and database.
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
randKey := securecookie.GenerateRandomKey(32)
randKey := GenerateRandomKey(32)
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
hashDigest := a.HashRaw(prefixedAPIKey)
apiKey := &portainer.APIKey{
@@ -54,8 +70,7 @@ func (a *apiKeyService) GenerateApiKey(user portainer.User, description string)
Digest: hashDigest,
}
err := a.apiKeyRepository.Create(apiKey)
if err != nil {
if err := a.apiKeyRepository.Create(apiKey); err != nil {
return "", nil, errors.Wrap(err, "Unable to create API key")
}
@@ -78,7 +93,6 @@ func (a *apiKeyService) GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey,
// GetDigestUserAndKey returns the user and api-key associated to a specified hash digest.
// A cache lookup is performed first; if the user/api-key is not found in the cache, respective database lookups are performed.
func (a *apiKeyService) GetDigestUserAndKey(digest string) (portainer.User, portainer.APIKey, error) {
// get api key from cache if possible
cachedUser, cachedKey, ok := a.cache.Get(digest)
if ok {
return cachedUser, cachedKey, nil
@@ -106,20 +120,21 @@ func (a *apiKeyService) UpdateAPIKey(apiKey *portainer.APIKey) error {
if err != nil {
return errors.Wrap(err, "Unable to retrieve API key")
}
a.cache.Set(apiKey.Digest, user, *apiKey)
return a.apiKeyRepository.Update(apiKey.ID, apiKey)
}
// DeleteAPIKey deletes an API key and removes the digest/api-key entry from the cache.
func (a *apiKeyService) DeleteAPIKey(apiKeyID portainer.APIKeyID) error {
// get api-key digest to remove from cache
apiKey, err := a.apiKeyRepository.Read(apiKeyID)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Unable to retrieve API key: %d", apiKeyID))
}
// delete the user/api-key from cache
a.cache.Delete(apiKey.Digest)
return a.apiKeyRepository.Delete(apiKeyID)
}

View File

@@ -15,7 +15,7 @@ import (
// abosolutePath should be an absolute path to a directory.
// Archive name will be <directoryName>.tar.gz and will be placed next to the directory.
func TarGzDir(absolutePath string) (string, error) {
targzPath := filepath.Join(absolutePath, fmt.Sprintf("%s.tar.gz", filepath.Base(absolutePath)))
targzPath := filepath.Join(absolutePath, filepath.Base(absolutePath)+".tar.gz")
outFile, err := os.Create(targzPath)
if err != nil {
return "", err

View File

@@ -1,7 +1,6 @@
package archive
import (
"fmt"
"os"
"os/exec"
"path"
@@ -24,7 +23,7 @@ func listFiles(dir string) []string {
return items
}
func Test_shouldCreateArhive(t *testing.T) {
func Test_shouldCreateArchive(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
@@ -34,12 +33,11 @@ func Test_shouldCreateArhive(t *testing.T) {
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
assert.Equal(t, filepath.Join(tmpdir, filepath.Base(tmpdir)+".tar.gz"), gzPath)
extractionDir := t.TempDir()
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
err = cmd.Run()
if err != nil {
if err := cmd.Run(); err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)
@@ -56,7 +54,7 @@ func Test_shouldCreateArhive(t *testing.T) {
wasExtracted("dir/.dotfile")
}
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
func Test_shouldCreateArchive2(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
@@ -66,12 +64,11 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
gzPath, err := TarGzDir(tmpdir)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
assert.Equal(t, filepath.Join(tmpdir, filepath.Base(tmpdir)+".tar.gz"), gzPath)
extractionDir := t.TempDir()
r, _ := os.Open(gzPath)
ExtractTarGz(r, extractionDir)
if err != nil {
if err := ExtractTarGz(r, extractionDir); err != nil {
t.Fatal("Failed to extract archive: ", err)
}
extractedFiles := listFiles(extractionDir)

View File

@@ -3,7 +3,7 @@ package ecr
import (
"context"
"encoding/base64"
"fmt"
"errors"
"strings"
"time"
)
@@ -15,7 +15,7 @@ func (s *Service) GetEncodedAuthorizationToken() (token *string, expiry *time.Ti
}
if len(getAuthorizationTokenOutput.AuthorizationData) == 0 {
err = fmt.Errorf("AuthorizationData is empty")
err = errors.New("AuthorizationData is empty")
return
}
@@ -50,7 +50,7 @@ func (s *Service) ParseAuthorizationToken(token string) (username string, passwo
splitToken := strings.Split(token, ":")
if len(splitToken) < 2 {
err = fmt.Errorf("invalid ECR authorization token")
err = errors.New("invalid ECR authorization token")
return
}

View File

@@ -21,6 +21,7 @@ const rwxr__r__ os.FileMode = 0o744
var filesToBackup = []string{
"certs",
"chisel",
"compose",
"config.json",
"custom_templates",
@@ -30,40 +31,13 @@ var filesToBackup = []string{
"portainer.key",
"portainer.pub",
"tls",
"chisel",
}
// Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file.
func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, filestorePath string) (string, error) {
unlock := gate.Lock()
defer unlock()
backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
return "", errors.Wrap(err, "Failed to create backup dir")
}
{
// new export
exportFilename := path.Join(backupDirPath, fmt.Sprintf("export-%d.json", time.Now().Unix()))
err := datastore.Export(exportFilename)
if err != nil {
log.Error().Err(err).Str("filename", exportFilename).Msg("failed to export")
} else {
log.Debug().Str("filename", exportFilename).Msg("file exported")
}
}
if err := backupDb(backupDirPath, datastore); err != nil {
return "", errors.Wrap(err, "Failed to backup database")
}
for _, filename := range filesToBackup {
err := filesystem.CopyPath(filepath.Join(filestorePath, filename), backupDirPath)
if err != nil {
return "", errors.Wrap(err, "Failed to create backup file")
}
backupDirPath, err := backupDatabaseAndFilesystem(gate, datastore, filestorePath)
if err != nil {
return "", err
}
archivePath, err := archive.TarGzDir(backupDirPath)
@@ -81,8 +55,40 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
return archivePath, nil
}
func backupDatabaseAndFilesystem(gate *offlinegate.OfflineGate, datastore dataservices.DataStore, filestorePath string) (string, error) {
unlock := gate.Lock()
defer unlock()
backupDirPath := filepath.Join(filestorePath, "backup", time.Now().Format("2006-01-02_15-04-05"))
if err := os.MkdirAll(backupDirPath, rwxr__r__); err != nil {
return "", errors.Wrap(err, "Failed to create backup dir")
}
// new export
exportFilename := path.Join(backupDirPath, fmt.Sprintf("export-%d.json", time.Now().Unix()))
if err := datastore.Export(exportFilename); err != nil {
log.Error().Err(err).Str("filename", exportFilename).Msg("failed to export")
} else {
log.Debug().Str("filename", exportFilename).Msg("file exported")
}
if err := backupDb(backupDirPath, datastore); err != nil {
return "", errors.Wrap(err, "Failed to backup database")
}
for _, filename := range filesToBackup {
if err := filesystem.CopyPath(filepath.Join(filestorePath, filename), backupDirPath); err != nil {
return "", errors.Wrap(err, "Failed to create backup file")
}
}
return backupDirPath, nil
}
func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
_, err := datastore.Backup(filepath.Join(backupDirPath, "portainer.db"))
dbFileName := datastore.Connection().GetDatabaseFileName()
_, err := datastore.Backup(filepath.Join(backupDirPath, dbFileName))
return err
}
@@ -93,7 +99,7 @@ func encrypt(path string, passphrase string) (string, error) {
}
defer in.Close()
outFileName := fmt.Sprintf("%s.encrypted", path)
outFileName := path + ".encrypted"
out, err := os.Create(outFileName)
if err != nil {
return "", err

View File

@@ -1,12 +0,0 @@
package build
import "runtime"
// Variables to be set during the build time
var BuildNumber string
var ImageTag string
var NodejsVersion string
var YarnVersion string
var WebpackVersion string
var GoVersion string = runtime.Version()
var GitCommit string

View File

@@ -1,75 +0,0 @@
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(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
if endpoint.Edge.AsyncMode {
return
}
service.mu.Lock()
tunnel := service.getTunnelDetails(endpoint.ID)
existingJobIndex := -1
for idx, existingJob := range tunnel.Jobs {
if existingJob.ID == edgeJob.ID {
existingJobIndex = idx
break
}
}
if existingJobIndex == -1 {
tunnel.Jobs = append(tunnel.Jobs, *edgeJob)
} else {
tunnel.Jobs[existingJobIndex] = *edgeJob
}
cache.Del(endpoint.ID)
service.mu.Unlock()
}
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
for endpointID, tunnel := range service.tunnelDetailsMap {
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()
}
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

@@ -19,7 +19,6 @@ import (
const (
tunnelCleanupInterval = 10 * time.Second
requiredTimeout = 15 * time.Second
activeTimeout = 4*time.Minute + 30*time.Second
pingTimeout = 3 * time.Second
)
@@ -28,32 +27,54 @@ const (
// It is used to start a reverse tunnel server and to manage the connection status of each tunnel
// connected to the tunnel server.
type Service struct {
serverFingerprint string
serverPort string
tunnelDetailsMap map[portainer.EndpointID]*portainer.TunnelDetails
dataStore dataservices.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
ProxyManager *proxy.Manager
mu sync.Mutex
fileService portainer.FileService
serverFingerprint string
serverPort string
activeTunnels map[portainer.EndpointID]*portainer.TunnelDetails
edgeJobs map[portainer.EndpointID][]portainer.EdgeJob
dataStore dataservices.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
ProxyManager *proxy.Manager
mu sync.RWMutex
fileService portainer.FileService
defaultCheckinInterval int
}
// NewService returns a pointer to a new instance of Service
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context, fileService portainer.FileService) *Service {
defaultCheckinInterval := portainer.DefaultEdgeAgentCheckinIntervalInSeconds
settings, err := dataStore.Settings().Settings()
if err == nil {
defaultCheckinInterval = settings.EdgeAgentCheckinInterval
} else {
log.Error().Err(err).Msg("unable to retrieve the settings from the database")
}
return &Service{
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
fileService: fileService,
activeTunnels: make(map[portainer.EndpointID]*portainer.TunnelDetails),
edgeJobs: make(map[portainer.EndpointID][]portainer.EdgeJob),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
fileService: fileService,
defaultCheckinInterval: defaultCheckinInterval,
}
}
// pingAgent ping the given agent so that the agent can keep the tunnel alive
func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
tunnel := service.GetTunnelDetails(endpointID)
requestURL := fmt.Sprintf("http://127.0.0.1:%d/ping", tunnel.Port)
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
tunnelAddr, err := service.TunnelAddr(endpoint)
if err != nil {
return err
}
requestURL := fmt.Sprintf("http://%s/ping", tunnelAddr)
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
if err != nil {
return err
@@ -76,47 +97,49 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
go func() {
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("max_alive_minutes", maxAlive.Minutes()).
Msg("KeepTunnelAlive: start")
go service.keepTunnelAlive(endpointID, ctx, maxAlive)
}
maxAliveTicker := time.NewTicker(maxAlive)
defer maxAliveTicker.Stop()
func (service *Service) keepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("max_alive_minutes", maxAlive.Minutes()).
Msg("KeepTunnelAlive: start")
pingTicker := time.NewTicker(tunnelCleanupInterval)
defer pingTicker.Stop()
maxAliveTicker := time.NewTicker(maxAlive)
defer maxAliveTicker.Stop()
for {
select {
case <-pingTicker.C:
service.SetTunnelStatusToActive(endpointID)
err := service.pingAgent(endpointID)
if err != nil {
log.Debug().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("KeepTunnelAlive: ping agent")
}
case <-maxAliveTicker.C:
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("timeout_minutes", maxAlive.Minutes()).
Msg("KeepTunnelAlive: tunnel keep alive timeout")
pingTicker := time.NewTicker(tunnelCleanupInterval)
defer pingTicker.Stop()
return
case <-ctx.Done():
err := ctx.Err()
for {
select {
case <-pingTicker.C:
service.UpdateLastActivity(endpointID)
if err := service.pingAgent(endpointID); err != nil {
log.Debug().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("KeepTunnelAlive: tunnel stop")
return
Msg("KeepTunnelAlive: ping agent")
}
case <-maxAliveTicker.C:
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("timeout_minutes", maxAlive.Minutes()).
Msg("KeepTunnelAlive: tunnel keep alive timeout")
return
case <-ctx.Done():
err := ctx.Err()
log.Debug().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("KeepTunnelAlive: tunnel stop")
return
}
}()
}
}
// StartTunnelServer starts a tunnel server on the specified addr and port.
@@ -126,7 +149,6 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
// The snapshotter is used in the tunnel status verification process.
func (service *Service) StartTunnelServer(addr, port string, snapshotService portainer.SnapshotService) error {
privateKeyFile, err := service.retrievePrivateKeyFile()
if err != nil {
return err
}
@@ -144,21 +166,21 @@ func (service *Service) StartTunnelServer(addr, port string, snapshotService por
service.serverFingerprint = chiselServer.GetFingerprint()
service.serverPort = port
err = chiselServer.Start(addr, port)
if err != nil {
if err := chiselServer.Start(addr, port); err != nil {
return err
}
service.chiselServer = chiselServer
// TODO: work-around Chisel default behavior.
// By default, Chisel will allow anyone to connect if no user exists.
username, password := generateRandomCredentials()
err = service.chiselServer.AddUser(username, password, "127.0.0.1")
if err != nil {
if err = service.chiselServer.AddUser(username, password, "127.0.0.1"); err != nil {
return err
}
service.snapshotService = snapshotService
go service.startTunnelVerificationLoop()
return nil
@@ -172,37 +194,39 @@ func (service *Service) StopTunnelServer() error {
func (service *Service) retrievePrivateKeyFile() (string, error) {
privateKeyFile := service.fileService.GetDefaultChiselPrivateKeyPath()
exist, _ := service.fileService.FileExists(privateKeyFile)
if !exist {
log.Debug().
Str("private-key", privateKeyFile).
Msg("Chisel private key file does not exist")
privateKey, err := ccrypto.GenerateKey("")
if err != nil {
log.Error().
Err(err).
Msg("Failed to generate chisel private key")
return "", err
}
err = service.fileService.StoreChiselPrivateKey(privateKey)
if err != nil {
log.Error().
Err(err).
Msg("Failed to save Chisel private key to disk")
return "", err
} else {
log.Info().
Str("private-key", privateKeyFile).
Msg("Generated a new Chisel private key file")
}
} else {
if exists, _ := service.fileService.FileExists(privateKeyFile); exists {
log.Info().
Str("private-key", privateKeyFile).
Msg("Found Chisel private key file on disk")
Msg("found Chisel private key file on disk")
return privateKeyFile, nil
}
log.Debug().
Str("private-key", privateKeyFile).
Msg("chisel private key file does not exist")
privateKey, err := ccrypto.GenerateKey("")
if err != nil {
log.Error().
Err(err).
Msg("failed to generate chisel private key")
return "", err
}
if err = service.fileService.StoreChiselPrivateKey(privateKey); err != nil {
log.Error().
Err(err).
Msg("failed to save Chisel private key to disk")
return "", err
}
log.Info().
Str("private-key", privateKeyFile).
Msg("generated a new Chisel private key file")
return privateKeyFile, nil
}
@@ -230,63 +254,45 @@ func (service *Service) startTunnelVerificationLoop() {
}
}
// checkTunnels finds the first tunnel that has not had any activity recently
// and attempts to take a snapshot, then closes it and returns
func (service *Service) checkTunnels() {
tunnels := make(map[portainer.EndpointID]portainer.TunnelDetails)
service.mu.RLock()
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 {
for endpointID, tunnel := range service.activeTunnels {
elapsed := time.Since(tunnel.LastActivity)
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
Float64("status_time_seconds", elapsed.Seconds()).
Float64("last_activity_seconds", elapsed.Seconds()).
Msg("environment tunnel monitoring")
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed > requiredTimeout {
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
Float64("status_time_seconds", elapsed.Seconds()).
Float64("timeout_seconds", requiredTimeout.Seconds()).
Msg("REQUIRED state timeout exceeded")
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed < activeTimeout {
continue
}
if tunnel.Status == portainer.EdgeAgentActive && elapsed > activeTimeout {
log.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
Float64("status_time_seconds", elapsed.Seconds()).
Float64("timeout_seconds", activeTimeout.Seconds()).
Msg("ACTIVE state timeout exceeded")
tunnelPort := tunnel.Port
err := service.snapshotEnvironment(endpointID, tunnel.Port)
if err != nil {
log.Error().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("unable to snapshot Edge environment")
}
service.mu.RUnlock()
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("last_activity_seconds", elapsed.Seconds()).
Float64("timeout_seconds", activeTimeout.Seconds()).
Msg("last activity timeout exceeded")
if err := service.snapshotEnvironment(endpointID, tunnelPort); err != nil {
log.Error().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("unable to snapshot Edge environment")
}
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
service.close(endpointID)
return
}
service.mu.RUnlock()
}
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {

View File

@@ -8,14 +8,22 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/require"
)
func TestPingAgentPanic(t *testing.T) {
endpointID := portainer.EndpointID(1)
endpoint := &portainer.Endpoint{
ID: 1,
EdgeID: "test-edge-id",
Type: portainer.EdgeAgentOnDockerEnvironment,
UserTrusted: true,
}
s := NewService(nil, nil, nil)
_, store := datastore.MustNewTestStore(t, true, true)
s := NewService(store, nil, nil)
defer func() {
require.Nil(t, recover())
@@ -36,10 +44,11 @@ func TestPingAgentPanic(t *testing.T) {
errCh <- srv.Serve(ln)
}()
s.getTunnelDetails(endpointID)
s.tunnelDetailsMap[endpointID].Port = ln.Addr().(*net.TCPAddr).Port
err = s.Open(endpoint)
require.NoError(t, err)
s.activeTunnels[endpoint.ID].Port = ln.Addr().(*net.TCPAddr).Port
require.Error(t, s.pingAgent(endpointID))
require.Error(t, s.pingAgent(endpoint.ID))
require.NoError(t, srv.Shutdown(context.Background()))
require.ErrorIs(t, <-errCh, http.ErrServerClosed)
}

View File

@@ -5,14 +5,18 @@ import (
"errors"
"fmt"
"math/rand"
"net"
"strings"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/pkg/libcrypto"
"github.com/dchest/uniuri"
"github.com/rs/zerolog/log"
)
const (
@@ -20,18 +24,191 @@ const (
maxAvailablePort = 65535
)
var (
ErrNonEdgeEnv = errors.New("cannot open a tunnel for non-edge environments")
ErrAsyncEnv = errors.New("cannot open a tunnel for async edge environments")
ErrInvalidEnv = errors.New("cannot open a tunnel for an invalid environment")
)
// Open will mark the tunnel as REQUIRED so the agent opens it
func (s *Service) Open(endpoint *portainer.Endpoint) error {
if !endpointutils.IsEdgeEndpoint(endpoint) {
return ErrNonEdgeEnv
}
if endpoint.Edge.AsyncMode {
return ErrAsyncEnv
}
if endpoint.ID == 0 || endpoint.EdgeID == "" || !endpoint.UserTrusted {
return ErrInvalidEnv
}
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.activeTunnels[endpoint.ID]; ok {
return nil
}
defer cache.Del(endpoint.ID)
tun := &portainer.TunnelDetails{
Status: portainer.EdgeAgentManagementRequired,
Port: s.getUnusedPort(),
LastActivity: time.Now(),
}
username, password := generateRandomCredentials()
if s.chiselServer != nil {
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tun.Port)
if err := s.chiselServer.AddUser(username, password, authorizedRemote); err != nil {
return err
}
}
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
if err != nil {
return err
}
tun.Credentials = credentials
s.activeTunnels[endpoint.ID] = tun
return nil
}
// close removes the tunnel from the map so the agent will close it
func (s *Service) close(endpointID portainer.EndpointID) {
s.mu.Lock()
defer s.mu.Unlock()
tun, ok := s.activeTunnels[endpointID]
if !ok {
return
}
if len(tun.Credentials) > 0 && s.chiselServer != nil {
user, _, _ := strings.Cut(tun.Credentials, ":")
s.chiselServer.DeleteUser(user)
}
if s.ProxyManager != nil {
s.ProxyManager.DeleteEndpointProxy(endpointID)
}
delete(s.activeTunnels, endpointID)
cache.Del(endpointID)
}
// Config returns the tunnel details needed for the agent to connect
func (s *Service) Config(endpointID portainer.EndpointID) portainer.TunnelDetails {
s.mu.RLock()
defer s.mu.RUnlock()
if tun, ok := s.activeTunnels[endpointID]; ok {
return *tun
}
return portainer.TunnelDetails{Status: portainer.EdgeAgentIdle}
}
// TunnelAddr returns the address of the local tunnel, including the port, it
// will block until the tunnel is ready
func (s *Service) TunnelAddr(endpoint *portainer.Endpoint) (string, error) {
if err := s.Open(endpoint); err != nil {
return "", err
}
tun := s.Config(endpoint.ID)
checkinInterval := time.Duration(s.tryEffectiveCheckinInterval(endpoint)) * time.Second
for t0 := time.Now(); ; {
if time.Since(t0) > 2*checkinInterval {
s.close(endpoint.ID)
return "", errors.New("unable to open the tunnel")
}
// Check if the tunnel is established
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: tun.Port})
if err != nil {
time.Sleep(checkinInterval / 100)
continue
}
conn.Close()
break
}
s.UpdateLastActivity(endpoint.ID)
return fmt.Sprintf("127.0.0.1:%d", tun.Port), nil
}
// tryEffectiveCheckinInterval avoids a potential deadlock by returning a
// previous known value after a timeout
func (s *Service) tryEffectiveCheckinInterval(endpoint *portainer.Endpoint) int {
ch := make(chan int, 1)
go func() {
ch <- edge.EffectiveCheckinInterval(s.dataStore, endpoint)
}()
select {
case <-time.After(50 * time.Millisecond):
s.mu.RLock()
defer s.mu.RUnlock()
return s.defaultCheckinInterval
case i := <-ch:
s.mu.Lock()
s.defaultCheckinInterval = i
s.mu.Unlock()
return i
}
}
// UpdateLastActivity sets the current timestamp to avoid the tunnel timeout
func (s *Service) UpdateLastActivity(endpointID portainer.EndpointID) {
s.mu.Lock()
defer s.mu.Unlock()
if tun, ok := s.activeTunnels[endpointID]; ok {
tun.LastActivity = time.Now()
}
}
// NOTE: it needs to be called with the lock acquired
// getUnusedPort is used to generate an unused random port in the dynamic port range.
// Dynamic ports (also called private ports) are 49152 to 65535.
func (service *Service) getUnusedPort() int {
port := randomInt(minAvailablePort, maxAvailablePort)
for _, tunnel := range service.tunnelDetailsMap {
for _, tunnel := range service.activeTunnels {
if tunnel.Port == port {
return service.getUnusedPort()
}
}
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port})
if err == nil {
conn.Close()
log.Debug().
Int("port", port).
Msg("selected port is in use, trying a different one")
return service.getUnusedPort()
}
return port
}
@@ -39,152 +216,10 @@ func randomInt(min, max int) int {
return min + rand.Intn(max-min)
}
// NOTE: it needs to be called with the lock acquired
func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
if tunnel, ok := service.tunnelDetailsMap[endpointID]; ok {
return tunnel
}
tunnel := &portainer.TunnelDetails{
Status: portainer.EdgeAgentIdle,
}
service.tunnelDetailsMap[endpointID] = tunnel
cache.Del(endpointID)
return tunnel
}
// GetTunnelDetails returns information about the tunnel associated to an environment(endpoint).
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portainer.TunnelDetails {
service.mu.Lock()
defer service.mu.Unlock()
return *service.getTunnelDetails(endpointID)
}
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
if endpoint.Edge.AsyncMode {
return portainer.TunnelDetails{}, errors.New("cannot open tunnel on async endpoint")
}
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentActive {
// update the LastActivity
service.SetTunnelStatusToActive(endpoint.ID)
}
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return portainer.TunnelDetails{}, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := service.dataStore.Settings().Settings()
if err != nil {
return portainer.TunnelDetails{}, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
}
return service.GetTunnelDetails(endpoint.ID), nil
}
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to ACTIVE.
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentActive
tunnel.Credentials = ""
tunnel.LastActivity = time.Now()
service.mu.Unlock()
cache.Del(endpointID)
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to IDLE.
// It removes any existing credentials associated to the tunnel.
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentIdle
tunnel.Port = 0
tunnel.LastActivity = time.Now()
credentials := tunnel.Credentials
if credentials != "" {
tunnel.Credentials = ""
if service.chiselServer != nil {
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
}
}
service.ProxyManager.DeleteEndpointProxy(endpointID)
service.mu.Unlock()
cache.Del(endpointID)
}
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to REQUIRED.
// If no port is currently associated to the tunnel, it will associate a random unused port to the tunnel
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
defer cache.Del(endpointID)
tunnel := service.getTunnelDetails(endpointID)
service.mu.Lock()
defer service.mu.Unlock()
if tunnel.Port == 0 {
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return err
}
tunnel.Status = portainer.EdgeAgentManagementRequired
tunnel.Port = service.getUnusedPort()
tunnel.LastActivity = time.Now()
username, password := generateRandomCredentials()
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
if service.chiselServer != nil {
err = service.chiselServer.AddUser(username, password, authorizedRemote)
if err != nil {
return err
}
}
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
if err != nil {
return err
}
tunnel.Credentials = credentials
}
return nil
}
func generateRandomCredentials() (string, string) {
username := uniuri.NewLen(8)
password := uniuri.NewLen(8)
return username, password
}

View File

@@ -17,24 +17,20 @@ import (
type Service struct{}
var (
errInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://")
errSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
errInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
errAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
ErrInvalidEndpointProtocol = errors.New("Invalid environment protocol: Portainer only supports unix://, npipe:// or tcp://")
ErrSocketOrNamedPipeNotFound = errors.New("Unable to locate Unix socket or named pipe")
ErrInvalidSnapshotInterval = errors.New("Invalid snapshot interval")
ErrAdminPassExcludeAdminPassFile = errors.New("Cannot use --admin-password with --admin-password-file")
)
// ParseFlags parse the CLI flags and return a portainer.Flags struct
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := &portainer.CLIFlags{
func CLIFlags() *portainer.CLIFlags {
return &portainer.CLIFlags{
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
AddrHTTPS: kingpin.Flag("bind-https", "Address and port to serve Portainer via https").Default(defaultHTTPSBindAddress).String(),
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
@@ -63,7 +59,15 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
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("NOCOLOR", "PRETTY", "JSON"),
KubectlShellImage: kingpin.Flag("kubectl-shell-image", "Kubectl shell image").Envar(portainer.KubectlShellImageEnvVar).Default(portainer.DefaultKubectlShellImage).String(),
}
}
// ParseFlags parse the CLI flags and return a portainer.Flags struct
func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
kingpin.Version(version)
flags := CLIFlags()
kingpin.Parse()
@@ -83,18 +87,16 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
err := validateEndpointURL(*flags.EndpointURL)
if err != nil {
if err := validateEndpointURL(*flags.EndpointURL); err != nil {
return err
}
err = validateSnapshotInterval(*flags.SnapshotInterval)
if err != nil {
if err := validateSnapshotInterval(*flags.SnapshotInterval); err != nil {
return err
}
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
return errAdminPassExcludeAdminPassFile
return ErrAdminPassExcludeAdminPassFile
}
return nil
@@ -116,15 +118,16 @@ func validateEndpointURL(endpointURL string) error {
}
if !strings.HasPrefix(endpointURL, "unix://") && !strings.HasPrefix(endpointURL, "tcp://") && !strings.HasPrefix(endpointURL, "npipe://") {
return errInvalidEndpointProtocol
return ErrInvalidEndpointProtocol
}
if strings.HasPrefix(endpointURL, "unix://") || strings.HasPrefix(endpointURL, "npipe://") {
socketPath := strings.TrimPrefix(endpointURL, "unix://")
socketPath = strings.TrimPrefix(socketPath, "npipe://")
if _, err := os.Stat(socketPath); err != nil {
if os.IsNotExist(err) {
return errSocketOrNamedPipeNotFound
return ErrSocketOrNamedPipeNotFound
}
return err
@@ -139,9 +142,8 @@ func validateSnapshotInterval(snapshotInterval string) error {
return nil
}
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval
if _, err := time.ParseDuration(snapshotInterval); err != nil {
return ErrInvalidSnapshotInterval
}
return nil

View File

@@ -19,7 +19,5 @@ func Confirm(message string) (bool, error) {
}
answer = strings.ReplaceAll(answer, "\n", "")
answer = strings.ToLower(answer)
return answer == "y" || answer == "yes", nil
return strings.EqualFold(answer, "y") || strings.EqualFold(answer, "yes"), nil
}

View File

@@ -54,7 +54,7 @@ func setLoggingMode(mode string) {
}
}
func formatMessage(i interface{}) string {
func formatMessage(i any) string {
if i == nil {
return ""
}

View File

@@ -1,6 +1,7 @@
package main
import (
"cmp"
"context"
"crypto/sha256"
"os"
@@ -9,7 +10,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/build"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
@@ -19,7 +19,7 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/demo"
"github.com/portainer/portainer/api/datastore/postinit"
"github.com/portainer/portainer/api/docker"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/exec"
@@ -30,7 +30,6 @@ import (
"github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/snapshot"
@@ -42,11 +41,15 @@ import (
"github.com/portainer/portainer/api/ldap"
"github.com/portainer/portainer/api/oauth"
"github.com/portainer/portainer/api/pendingactions"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/api/pendingactions/handlers"
"github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/build"
"github.com/portainer/portainer/pkg/featureflags"
"github.com/portainer/portainer/pkg/libhelm"
"github.com/portainer/portainer/pkg/libstack"
libhelmtypes "github.com/portainer/portainer/pkg/libhelm/types"
"github.com/portainer/portainer/pkg/libstack/compose"
"github.com/gofrs/uuid"
@@ -54,14 +57,14 @@ import (
)
func initCLI() *portainer.CLIFlags {
var cliService portainer.CLIService = &cli.Service{}
cliService := &cli.Service{}
flags, err := cliService.ParseFlags(portainer.APIVersion)
if err != nil {
log.Fatal().Err(err).Msg("failed parsing flags")
}
err = cliService.ValidateFlags(flags)
if err != nil {
if err := cliService.ValidateFlags(flags); err != nil {
log.Fatal().Err(err).Msg("failed validating flags")
}
@@ -91,15 +94,15 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
log.Fatal().Msg("failed creating database connection: expecting a boltdb database type but a different one was received")
}
store := datastore.NewStore(*flags.Data, fileService, connection)
store := datastore.NewStore(flags, fileService, connection)
isNew, err := store.Open()
if err != nil {
log.Fatal().Err(err).Msg("failed opening store")
}
if *flags.Rollback {
err := store.Rollback(false)
if err != nil {
if err := store.Rollback(false); err != nil {
log.Fatal().Err(err).Msg("failed rolling back")
}
@@ -108,8 +111,7 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
// Init sets some defaults - it's basically a migration
err = store.Init()
if err != nil {
if err := store.Init(); err != nil {
log.Fatal().Err(err).Msg("failed initializing data store")
}
@@ -119,7 +121,7 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
log.Fatal().Err(err).Msg("failed generating instance id")
}
migratorInstance := migrator.NewMigrator(&migrator.MigratorParameters{})
migratorInstance := migrator.NewMigrator(&migrator.MigratorParameters{Flags: flags})
migratorCount := migratorInstance.GetMigratorCountOfCurrentAPIVersion()
// from MigrateData
@@ -131,25 +133,23 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
store.VersionService.UpdateVersion(&v)
err = updateSettingsFromFlags(store, flags)
if err != nil {
if err := updateSettingsFromFlags(store, flags); err != nil {
log.Fatal().Err(err).Msg("failed updating settings from flags")
}
} else {
err = store.MigrateData()
if err != nil {
if err := store.MigrateData(); err != nil {
log.Fatal().Err(err).Msg("failed migration")
}
}
err = updateSettingsFromFlags(store, flags)
if err != nil {
if err := updateSettingsFromFlags(store, flags); err != nil {
log.Fatal().Err(err).Msg("failed updating settings from flags")
}
// this is for the db restore functionality - needs more tests.
go func() {
<-shutdownCtx.Done()
defer connection.Close()
}()
@@ -166,32 +166,12 @@ func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersi
return v.SchemaVersion == serverVersion && v.Edition == serverEdition
}
func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager)
if err != nil {
log.Fatal().Err(err).Msg("failed creating compose manager")
}
return composeWrapper
}
func initSwarmStackManager(
assetsPath string,
configPath string,
signatureService portainer.DigitalSignatureService,
fileService portainer.FileService,
reverseTunnelService portainer.ReverseTunnelService,
dataStore dataservices.DataStore,
) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
}
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
}
func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, error) {
return libhelm.NewHelmPackageManager(libhelm.HelmConfig{BinaryPath: assetsPath})
func initHelmPackageManager() (libhelmtypes.HelmPackageManager, error) {
return libhelm.NewHelmPackageManager()
}
func initAPIKeyService(datastore dataservices.DataStore) apikey.APIKeyService {
@@ -203,36 +183,16 @@ func initJWTService(userSessionTimeout string, dataStore dataservices.DataStore)
userSessionTimeout = portainer.DefaultUserSessionTimeout
}
jwtService, err := jwt.NewService(userSessionTimeout, dataStore)
if err != nil {
return nil, err
}
return jwtService, nil
return jwt.NewService(userSessionTimeout, dataStore)
}
func initDigitalSignatureService() portainer.DigitalSignatureService {
return crypto.NewECDSAService(os.Getenv("AGENT_SECRET"))
}
func initCryptoService() portainer.CryptoService {
return &crypto.Service{}
}
func initLDAPService() portainer.LDAPService {
return &ldap.Service{}
}
func initOAuthService() portainer.OAuthService {
return oauth.NewService()
}
func initGitService(ctx context.Context) portainer.GitService {
return git.NewService(ctx)
}
func initSSLService(addr, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
slices := strings.Split(addr, ":")
host := slices[0]
if host == "" {
host = "0.0.0.0"
@@ -240,22 +200,13 @@ func initSSLService(addr, certPath, keyPath string, fileService portainer.FileSe
sslService := ssl.NewService(fileService, dataStore, shutdownTrigger)
err := sslService.Init(host, certPath, keyPath)
if err != nil {
if err := sslService.Init(host, certPath, keyPath); err != nil {
return nil, err
}
return sslService, nil
}
func initDockerClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *dockerclient.ClientFactory {
return dockerclient.NewClientFactory(signatureService, reverseTunnelService)
}
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
}
func initSnapshotService(
snapshotIntervalFromFlag string,
dataStore dataservices.DataStore,
@@ -288,34 +239,21 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
return err
}
if *flags.SnapshotInterval != "" {
settings.SnapshotInterval = *flags.SnapshotInterval
}
if *flags.Logo != "" {
settings.LogoURL = *flags.Logo
}
if *flags.EnableEdgeComputeFeatures {
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
}
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
}
settings.SnapshotInterval = cmp.Or(*flags.SnapshotInterval, settings.SnapshotInterval)
settings.LogoURL = cmp.Or(*flags.Logo, settings.LogoURL)
settings.EnableEdgeComputeFeatures = cmp.Or(*flags.EnableEdgeComputeFeatures, settings.EnableEdgeComputeFeatures)
settings.TemplatesURL = cmp.Or(*flags.Templates, settings.TemplatesURL)
if *flags.Labels != nil {
settings.BlackListedLabels = *flags.Labels
}
settings.AgentSecret = ""
if agentKey, ok := os.LookupEnv("AGENT_SECRET"); ok {
settings.AgentSecret = agentKey
} else {
settings.AgentSecret = ""
}
err = dataStore.Settings().UpdateSettings(settings)
if err != nil {
if err := dataStore.Settings().UpdateSettings(settings); err != nil {
return err
}
@@ -338,6 +276,7 @@ func loadAndParseKeyPair(fileService portainer.FileService, signatureService por
if err != nil {
return err
}
return signatureService.ParseKeyPair(private, public)
}
@@ -346,7 +285,9 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
if err != nil {
return err
}
privateHeader, publicHeader := signatureService.PEMHeaders()
return fileService.StoreKeyPair(private, public, privateHeader, publicHeader)
}
@@ -359,6 +300,7 @@ func initKeyPair(fileService portainer.FileService, signatureService portainer.D
if existingKeyPair {
return loadAndParseKeyPair(fileService, signatureService)
}
return generateAndStoreKeyPair(fileService, signatureService)
}
@@ -376,6 +318,7 @@ func loadEncryptionSecretKey(keyfilename string) []byte {
// return a 32 byte hash of the secret (required for AES)
hash := sha256.Sum256(content)
return hash[:]
}
@@ -420,17 +363,17 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Err(err).Msg("failed initializing JWT service")
}
ldapService := initLDAPService()
ldapService := &ldap.Service{}
oauthService := initOAuthService()
oauthService := oauth.NewService()
gitService := initGitService(shutdownCtx)
gitService := git.NewService(shutdownCtx)
openAMTService := openamt.NewService()
cryptoService := initCryptoService()
cryptoService := &crypto.Service{}
digitalSignatureService := initDigitalSignatureService()
signatureService := initDigitalSignatureService()
edgeStacksService := edgestacks.NewService(dataStore)
@@ -444,77 +387,71 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Err(err).Msg("failed to get SSL settings")
}
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
if err := initKeyPair(fileService, signatureService); err != nil {
log.Fatal().Err(err).Msg("failed initializing key pair")
}
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx, fileService)
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
dockerClientFactory := dockerclient.NewClientFactory(signatureService, reverseTunnelService)
kubernetesClientFactory, err := kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing Kubernetes Client Factory service")
}
authorizationService := authorization.NewService(dataStore)
authorizationService.K8sClientFactory = kubernetesClientFactory
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory, authorizationService, shutdownCtx)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing snapshot service")
}
snapshotService.Start()
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
proxyManager := proxy.NewManager(kubernetesClientFactory)
reverseTunnelService.ProxyManager = proxyManager
dockerConfigPath := fileService.GetDockerConfigPath()
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing compose deployer")
}
composeDeployer := compose.NewComposeDeployer()
composeStackManager := initComposeStackManager(composeDeployer, proxyManager)
composeStackManager := exec.NewComposeStackManager(composeDeployer, proxyManager, dataStore)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
swarmStackManager, err := exec.NewSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
}
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, *flags.Assets)
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory)
pendingActionsService.RegisterHandler(actions.CleanNAPWithOverridePolicies, handlers.NewHandlerCleanNAPWithOverridePolicies(authorizationService, dataStore))
pendingActionsService.RegisterHandler(actions.DeletePortainerK8sRegistrySecrets, handlers.NewHandlerDeleteRegistrySecrets(authorizationService, dataStore, kubernetesClientFactory))
pendingActionsService.RegisterHandler(actions.PostInitMigrateEnvironment, handlers.NewHandlerPostInitMigrateEnvironment(authorizationService, dataStore, kubernetesClientFactory, dockerClientFactory, *flags.Assets, kubernetesDeployer))
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing snapshot service")
}
snapshotService.Start()
proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
helmPackageManager, err := initHelmPackageManager()
if err != nil {
log.Fatal().Err(err).Msg("failed initializing helm package manager")
}
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil {
log.Fatal().Err(err).Msg("failed loading edge jobs from database")
}
applicationStatus := initStatus(instanceID)
demoService := demo.NewService()
if *flags.DemoEnvironment {
err := demoService.Init(dataStore, cryptoService)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing demo environment")
}
}
// channel to control when the admin user is created
adminCreationDone := make(chan struct{}, 1)
go endpointutils.InitEndpoint(shutdownCtx, adminCreationDone, flags, dataStore, snapshotService)
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
if err != nil {
@@ -537,14 +474,14 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
if len(users) == 0 {
log.Info().Msg("created admin user with the given password.")
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
Password: adminPasswordHash,
}
err := dataStore.User().Create(user)
if err != nil {
if err := dataStore.User().Create(user); err != nil {
log.Fatal().Err(err).Msg("failed creating admin user")
}
@@ -555,8 +492,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
}
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
if err := reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService); err != nil {
log.Fatal().Err(err).Msg("failed starting tunnel server")
}
@@ -569,7 +505,20 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal().Msg("failed to fetch SSL settings from DB")
}
upgradeService, err := upgrade.NewService(*flags.Assets, composeDeployer, kubernetesClientFactory)
platformService, err := platform.NewService(dataStore)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing platform service")
}
upgradeService, err := upgrade.NewService(
*flags.Assets,
kubernetesClientFactory,
dockerClientFactory,
composeStackManager,
dataStore,
fileService,
stackDeployer,
)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing upgrade service")
}
@@ -578,10 +527,12 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
// 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(
postInitMigrator := postinit.NewPostInitMigrator(
kubernetesClientFactory,
dockerClientFactory,
dataStore,
*flags.Assets,
kubernetesDeployer,
)
if err := postInitMigrator.PostInitMigrate(); err != nil {
log.Fatal().Err(err).Msg("failure during post init migrations")
@@ -612,7 +563,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeClusterAccessService: kubeClusterAccessService,
SignatureService: digitalSignatureService,
SignatureService: signatureService,
SnapshotService: snapshotService,
SSLService: sslService,
DockerClientFactory: dockerClientFactory,
@@ -621,10 +572,10 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
DemoService: demoService,
UpgradeService: upgradeService,
AdminCreationDone: adminCreationDone,
PendingActionsService: pendingActionsService,
PlatformService: platformService,
}
}
@@ -639,6 +590,7 @@ func main() {
for {
server := buildServer(flags)
log.Info().
Str("version", portainer.APIVersion).
Str("build_number", build.BuildNumber).
@@ -650,6 +602,7 @@ func main() {
Msg("starting Portainer")
err := server.Start()
log.Info().Err(err).Msg("HTTP server exited")
}
}

View File

@@ -0,0 +1,148 @@
// Package concurrent provides utilities for running multiple functions concurrently in Go.
// For example, many kubernetes calls can take a while to fulfill. Oftentimes in Portainer
// we need to get a list of objects from multiple kubernetes REST APIs. We can often call these
// apis concurrently to speed up the response time.
// This package provides a clean way to do just that.
//
// Examples:
// The ConfigMaps and Secrets function converted using concurrent.Run.
/*
// GetConfigMapsAndSecrets gets all the ConfigMaps AND all the Secrets for a
// given namespace in a k8s endpoint. The result is a list of both config maps
// and secrets. The IsSecret boolean property indicates if a given struct is a
// secret or configmap.
func (kcl *KubeClient) GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) {
// use closures to capture the current kube client and namespace by declaring wrapper functions
// that match the interface signature for concurrent.Func
listConfigMaps := func(ctx context.Context) (any, error) {
return kcl.cli.CoreV1().ConfigMaps(namespace).List(context.Background(), meta.ListOptions{})
}
listSecrets := func(ctx context.Context) (any, error) {
return kcl.cli.CoreV1().Secrets(namespace).List(context.Background(), meta.ListOptions{})
}
// run the functions concurrently and wait for results. We can also pass in a context to cancel.
// e.g. Deadline timer.
results, err := concurrent.Run(context.TODO(), listConfigMaps, listSecrets)
if err != nil {
return nil, err
}
var configMapList *core.ConfigMapList
var secretList *core.SecretList
for _, r := range results {
switch v := r.Result.(type) {
case *core.ConfigMapList:
configMapList = v
case *core.SecretList:
secretList = v
}
}
// TODO: Applications
var combined []models.K8sConfigMapOrSecret
for _, m := range configMapList.Items {
var cm models.K8sConfigMapOrSecret
cm.UID = string(m.UID)
cm.Name = m.Name
cm.Namespace = m.Namespace
cm.Annotations = m.Annotations
cm.Data = m.Data
cm.CreationDate = m.CreationTimestamp.Time.UTC().Format(time.RFC3339)
combined = append(combined, cm)
}
for _, s := range secretList.Items {
var secret models.K8sConfigMapOrSecret
secret.UID = string(s.UID)
secret.Name = s.Name
secret.Namespace = s.Namespace
secret.Annotations = s.Annotations
secret.Data = msbToMss(s.Data)
secret.CreationDate = s.CreationTimestamp.Time.UTC().Format(time.RFC3339)
secret.IsSecret = true
secret.SecretType = string(s.Type)
combined = append(combined, secret)
}
return combined, nil
}
*/
package concurrent
import (
"context"
"sync"
)
// Result contains the result and any error returned from running a client task function
type Result struct {
Result any // the result of running the task function
Err error // any error that occurred while running the task function
}
// Func is a function returns a result or error
type Func func(ctx context.Context) (any, error)
// Run runs a list of functions returns the results
func Run(ctx context.Context, maxConcurrency int, tasks ...Func) ([]Result, error) {
var wg sync.WaitGroup
resultsChan := make(chan Result, len(tasks))
taskChan := make(chan Func, len(tasks))
localCtx, cancelCtx := context.WithCancel(ctx)
defer cancelCtx()
runTask := func() {
defer wg.Done()
for fn := range taskChan {
result, err := fn(localCtx)
resultsChan <- Result{Result: result, Err: err}
}
}
// Set maxConcurrency to the number of tasks if zero or negative
if maxConcurrency <= 0 {
maxConcurrency = len(tasks)
}
// Start worker goroutines
for range maxConcurrency {
wg.Add(1)
go runTask()
}
// Add tasks to the task channel
for _, fn := range tasks {
taskChan <- fn
}
// Close the task channel to signal workers to stop when all tasks are done
close(taskChan)
// Wait for all workers to complete
wg.Wait()
close(resultsChan)
// Collect the results and cancel on error
results := make([]Result, 0, len(tasks))
for r := range resultsChan {
if r.Err != nil {
cancelCtx()
return nil, r.Err
}
results = append(results, r)
}
return results, nil
}

View File

@@ -5,22 +5,21 @@ import (
)
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
GetObject(bucketName string, key []byte, object any) error
GetAll(bucketName string, obj any, append func(o any) (any, error)) error
GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, append func(o any) (any, error)) error
}
type Transaction interface {
ReadTransaction
SetServiceName(bucketName string) error
UpdateObject(bucketName string, key []byte, object interface{}) error
UpdateObject(bucketName string, key []byte, object any) error
DeleteObject(bucketName string, key []byte) error
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
CreateObject(bucketName string, fn func(uint64) (int, any)) error
CreateObjectWithId(bucketName string, id int, obj any) error
CreateObjectWithStringId(bucketName string, id []byte, obj any) error
DeleteAllObjects(bucketName string, obj any, matching func(o any) (id int, ok bool)) error
GetNextIdentifier(bucketName string) int
}
@@ -41,13 +40,14 @@ type Connection interface {
GetDatabaseFileName() string
GetDatabaseFilePath() string
GetStorePath() string
GetDatabaseFileSize() (int64, error)
IsEncryptedStore() bool
NeedsEncryptionMigration() (bool, error)
SetEncrypted(encrypted bool)
BackupMetadata() (map[string]interface{}, error)
RestoreMetadata(s map[string]interface{}) error
BackupMetadata() (map[string]any, error)
RestoreMetadata(s map[string]any) error
UpdateObjectFunc(bucketName string, key []byte, object any, updateFn func()) error
ConvertToKey(v int) []byte

View File

@@ -31,8 +31,7 @@ const (
// AesEncrypt reads from input, encrypts with AES-256 and writes to output. passphrase is used to generate an encryption key
func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
err := aesEncryptGCM(input, output, passphrase)
if err != nil {
if err := aesEncryptGCM(input, output, passphrase); err != nil {
return fmt.Errorf("error encrypting file: %w", err)
}
@@ -142,7 +141,7 @@ func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
}
if string(header) != aesGcmHeader {
return nil, fmt.Errorf("invalid header")
return nil, errors.New("invalid header")
}
// Read salt
@@ -194,8 +193,7 @@ func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
return nil, err
}
_, err = buf.Write(plaintext)
if err != nil {
if _, err := buf.Write(plaintext); err != nil {
return nil, err
}

View File

@@ -22,6 +22,12 @@ func CreateTLSConfiguration() *tls.Config {
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
},
}
}

View File

@@ -8,6 +8,7 @@ import (
"math"
"os"
"path"
"strconv"
"time"
portainer "github.com/portainer/portainer/api"
@@ -61,6 +62,15 @@ func (connection *DbConnection) GetStorePath() string {
return connection.Path
}
func (connection *DbConnection) GetDatabaseFileSize() (int64, error) {
file, err := os.Stat(connection.GetDatabaseFilePath())
if err != nil {
return 0, fmt.Errorf("Failed to stat database file path: %s err: %w", connection.GetDatabaseFilePath(), err)
}
return file.Size(), nil
}
func (connection *DbConnection) SetEncrypted(flag bool) {
connection.isEncrypted = flag
}
@@ -73,7 +83,6 @@ func (connection *DbConnection) IsEncryptedStore() bool {
// NeedsEncryptionMigration returns true if database encryption is enabled and
// we have an un-encrypted DB that requires migration to an encrypted DB
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
@@ -121,11 +130,11 @@ func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
// Open opens and initializes the BoltDB database.
func (connection *DbConnection) Open() error {
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
// Now we open the db
databasePath := connection.GetDatabaseFilePath()
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
Timeout: 1 * time.Second,
InitialMmapSize: connection.InitialMmapSize,
@@ -178,6 +187,7 @@ func (connection *DbConnection) ViewTx(fn func(portainer.Transaction) error) err
func (connection *DbConnection) BackupTo(w io.Writer) error {
return connection.View(func(tx *bolt.Tx) error {
_, err := tx.WriteTo(w)
return err
})
}
@@ -192,6 +202,7 @@ func (connection *DbConnection) ExportRaw(filename string) error {
if err != nil {
return err
}
return os.WriteFile(filename, b, 0600)
}
@@ -201,6 +212,7 @@ func (connection *DbConnection) ExportRaw(filename string) error {
func (connection *DbConnection) ConvertToKey(v int) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
@@ -212,7 +224,7 @@ func keyToString(b []byte) string {
v := binary.BigEndian.Uint64(b)
if v <= math.MaxInt32 {
return fmt.Sprintf("%d", v)
return strconv.FormatUint(v, 10)
}
return string(b)
@@ -226,7 +238,7 @@ func (connection *DbConnection) SetServiceName(bucketName string) error {
}
// GetObject is a generic function used to retrieve an unmarshalled object from a database.
func (connection *DbConnection) GetObject(bucketName string, key []byte, object interface{}) error {
func (connection *DbConnection) GetObject(bucketName string, key []byte, object any) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(bucketName, key, object)
})
@@ -241,7 +253,7 @@ func (connection *DbConnection) getEncryptionKey() []byte {
}
// UpdateObject is a generic function used to update an object inside a database.
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object any) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.UpdateObject(bucketName, key, object)
})
@@ -282,7 +294,7 @@ func (connection *DbConnection) DeleteObject(bucketName string, key []byte) erro
// 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, obj interface{}, matching func(o interface{}) (id int, ok bool)) error {
func (connection *DbConnection) DeleteAllObjects(bucketName string, obj any, matching func(o any) (id int, ok bool)) error {
return connection.UpdateTx(func(tx portainer.Transaction) error {
return tx.DeleteAllObjects(bucketName, obj, matching)
})
@@ -301,71 +313,64 @@ 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 {
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, any)) error {
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 {
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj any) error {
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 {
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj any) error {
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 {
func (connection *DbConnection) GetAll(bucketName string, obj any, appendFn func(o any) (any, error)) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAll(bucketName, obj, append)
return tx.GetAll(bucketName, obj, appendFn)
})
}
// TODO: decide which Unmarshal to use, and use one...
func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, appendFn func(o any) (any, error)) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAllWithJsoniter(bucketName, obj, append)
})
}
func (connection *DbConnection) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, append func(o interface{}) (interface{}, error)) error {
return connection.ViewTx(func(tx portainer.Transaction) error {
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, append)
return tx.GetAllWithKeyPrefix(bucketName, keyPrefix, obj, appendFn)
})
}
// BackupMetadata will return a copy of the boltdb sequence numbers for all buckets.
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
buckets := map[string]interface{}{}
func (connection *DbConnection) BackupMetadata() (map[string]any, error) {
buckets := map[string]any{}
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
return tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil
})
return err
})
return buckets, err
}
// RestoreMetadata will restore the boltdb sequence numbers for all buckets.
func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error {
func (connection *DbConnection) RestoreMetadata(s map[string]any) error {
var err error
for bucketName, v := range s {
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
if !ok {
log.Error().Str("bucket", bucketName).Msg("failed to restore metadata to bucket, skipped")
continue
}

View File

@@ -87,10 +87,7 @@ func Test_NeedsEncryptionMigration(t *testing.T) {
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
connection := DbConnection{Path: dir}
if tc.dbname == "both" {

View File

@@ -8,8 +8,8 @@ import (
bolt "go.etcd.io/bbolt"
)
func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
buckets := map[string]interface{}{}
func backupMetadata(connection *bolt.DB) (map[string]any, error) {
buckets := map[string]any{}
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
@@ -39,7 +39,7 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
}
defer connection.Close()
backup := make(map[string]interface{})
backup := make(map[string]any)
if metadata {
meta, err := backupMetadata(connection)
if err != nil {
@@ -49,10 +49,10 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
backup["__metadata"] = meta
}
err = connection.View(func(tx *bolt.Tx) error {
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
if err := connection.View(func(tx *bolt.Tx) error {
return tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
var list []interface{}
var list []any
version := make(map[string]string)
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
@@ -60,7 +60,7 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
continue
}
var obj interface{}
var obj any
err := c.UnmarshalObject(v, &obj)
if err != nil {
log.Error().
@@ -84,27 +84,22 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
return nil
}
if len(list) > 0 {
if bucketName == "ssl" ||
bucketName == "settings" ||
bucketName == "tunnel_server" {
backup[bucketName] = nil
if len(list) > 0 {
backup[bucketName] = list[0]
}
return nil
if bucketName == "ssl" ||
bucketName == "settings" ||
bucketName == "tunnel_server" {
backup[bucketName] = nil
if len(list) > 0 {
backup[bucketName] = list[0]
}
backup[bucketName] = list
return nil
}
backup[bucketName] = list
return nil
})
return err
})
if err != nil {
}); err != nil {
return []byte("{}"), err
}

View File

@@ -5,17 +5,16 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"fmt"
"io"
"github.com/pkg/errors"
"github.com/segmentio/encoding/json"
)
var errEncryptedStringTooShort = fmt.Errorf("encrypted string too short")
var errEncryptedStringTooShort = errors.New("encrypted string too short")
// MarshalObject encodes an object to binary format
func (connection *DbConnection) MarshalObject(object interface{}) ([]byte, error) {
func (connection *DbConnection) MarshalObject(object any) ([]byte, error) {
buf := &bytes.Buffer{}
// Special case for the VERSION bucket. Here we're not using json
@@ -39,7 +38,7 @@ func (connection *DbConnection) MarshalObject(object interface{}) ([]byte, error
}
// UnmarshalObject decodes an object from binary data
func (connection *DbConnection) UnmarshalObject(data []byte, object interface{}) error {
func (connection *DbConnection) UnmarshalObject(data []byte, object any) error {
var err error
if connection.getEncryptionKey() != nil {
data, err = decrypt(data, connection.getEncryptionKey())
@@ -47,8 +46,8 @@ func (connection *DbConnection) UnmarshalObject(data []byte, object interface{})
return errors.Wrap(err, "Failed decrypting object")
}
}
e := json.Unmarshal(data, object)
if e != nil {
if e := json.Unmarshal(data, object); e != nil {
// Special case for the VERSION bucket. Here we're not using json
// So we need to return it as a string
s, ok := object.(*string)
@@ -58,6 +57,7 @@ func (connection *DbConnection) UnmarshalObject(data []byte, object interface{})
*s = string(data)
}
return err
}
@@ -70,22 +70,20 @@ func encrypt(plaintext []byte, passphrase []byte) (encrypted []byte, err error)
if err != nil {
return encrypted, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return encrypted, err
}
ciphertextByte := gcm.Seal(
nonce,
nonce,
plaintext,
nil)
return ciphertextByte, nil
return gcm.Seal(nonce, nonce, plaintext, nil), nil
}
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
if string(encrypted) == "false" {
return []byte("false"), nil
}
block, err := aes.NewCipher(passphrase)
if err != nil {
return encrypted, errors.Wrap(err, "Error creating cypher block")
@@ -102,11 +100,8 @@ func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err err
}
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
plaintextByte, err = gcm.Open(
nil,
nonce,
ciphertextByteClean,
nil)
plaintextByte, err = gcm.Open(nil, nonce, ciphertextByteClean, nil)
if err != nil {
return encrypted, errors.Wrap(err, "Error decrypting text")
}

View File

@@ -25,7 +25,7 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
uuid := uuid.Must(uuid.NewV4())
tests := []struct {
object interface{}
object any
expected string
}{
{
@@ -57,7 +57,7 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
expected: uuid.String(),
},
{
object: map[string]interface{}{"key": "value"},
object: map[string]any{"key": "value"},
expected: `{"key":"value"}`,
},
{
@@ -73,11 +73,11 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
expected: `["1","2","3"]`,
},
{
object: []map[string]interface{}{{"key1": "value1"}, {"key2": "value2"}},
object: []map[string]any{{"key1": "value1"}, {"key2": "value2"}},
expected: `[{"key1":"value1"},{"key2":"value2"}]`,
},
{
object: []interface{}{1, "2", false, map[string]interface{}{"key1": "value1"}},
object: []any{1, "2", false, map[string]any{"key1": "value1"}},
expected: `[1,"2",false,{"key1":"value1"}]`,
},
}

View File

@@ -20,7 +20,7 @@ func (tx *DbTransaction) SetServiceName(bucketName string) error {
return err
}
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interface{}) error {
func (tx *DbTransaction) GetObject(bucketName string, key []byte, object any) error {
bucket := tx.tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
@@ -31,7 +31,7 @@ func (tx *DbTransaction) GetObject(bucketName string, key []byte, object interfa
return tx.conn.UnmarshalObject(value, object)
}
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object interface{}) error {
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object any) error {
data, err := tx.conn.MarshalObject(object)
if err != nil {
return err
@@ -46,7 +46,7 @@ func (tx *DbTransaction) DeleteObject(bucketName string, key []byte) error {
return bucket.Delete(key)
}
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, matchingFn func(o interface{}) (id int, ok bool)) error {
func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj any, matchingFn func(o any) (id int, ok bool)) error {
var ids []int
bucket := tx.tx.Bucket([]byte(bucketName))
@@ -74,16 +74,18 @@ func (tx *DbTransaction) DeleteAllObjects(bucketName string, obj interface{}, ma
func (tx *DbTransaction) GetNextIdentifier(bucketName string) int {
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")
log.Error().Err(err).Str("bucket", bucketName).Msg("failed to get the next identifier")
return 0
}
return int(id)
}
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, any)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
seqId, _ := bucket.NextSequence()
@@ -97,7 +99,7 @@ func (tx *DbTransaction) CreateObject(bucketName string, fn func(uint64) (int, i
return bucket.Put(tx.conn.ConvertToKey(id), data)
}
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj any) error {
bucket := tx.tx.Bucket([]byte(bucketName))
data, err := tx.conn.MarshalObject(obj)
if err != nil {
@@ -107,7 +109,7 @@ func (tx *DbTransaction) CreateObjectWithId(bucketName string, id int, obj inter
return bucket.Put(tx.conn.ConvertToKey(id), data)
}
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte, obj any) error {
bucket := tx.tx.Bucket([]byte(bucketName))
data, err := tx.conn.MarshalObject(obj)
if err != nil {
@@ -117,7 +119,7 @@ func (tx *DbTransaction) CreateObjectWithStringId(bucketName string, id []byte,
return bucket.Put(id, data)
}
func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
func (tx *DbTransaction) GetAll(bucketName string, obj any, appendFn func(o any) (any, error)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
return bucket.ForEach(func(k []byte, v []byte) error {
@@ -130,20 +132,7 @@ func (tx *DbTransaction) GetAll(bucketName string, obj interface{}, appendFn fun
})
}
func (tx *DbTransaction) GetAllWithJsoniter(bucketName string, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
bucket := tx.tx.Bucket([]byte(bucketName))
return bucket.ForEach(func(k []byte, v []byte) error {
err := tx.conn.UnmarshalObject(v, obj)
if err == nil {
obj, err = appendFn(obj)
}
return err
})
}
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj interface{}, appendFn func(o interface{}) (interface{}, error)) error {
func (tx *DbTransaction) GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, appendFn func(o any) (any, 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() {

View File

@@ -21,8 +21,7 @@ type Service struct {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
if err := connection.SetServiceName(BucketName); err != nil {
return nil, err
}
@@ -41,7 +40,7 @@ func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer
err := service.Connection.GetAll(
BucketName,
&portainer.APIKey{},
func(obj interface{}) (interface{}, error) {
func(obj any) (any, error) {
record, ok := obj.(*portainer.APIKey)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
@@ -62,11 +61,11 @@ func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer
// Note: there is a 1-to-1 mapping of api-key and digest
func (service *Service) GetAPIKeyByDigest(digest string) (*portainer.APIKey, error) {
var k *portainer.APIKey
stop := fmt.Errorf("ok")
stop := errors.New("ok")
err := service.Connection.GetAll(
BucketName,
&portainer.APIKey{},
func(obj interface{}) (interface{}, error) {
func(obj any) (any, error) {
key, ok := obj.(*portainer.APIKey)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
@@ -95,7 +94,7 @@ func (service *Service) GetAPIKeyByDigest(digest string) (*portainer.APIKey, err
func (service *Service) Create(record *portainer.APIKey) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
record.ID = portainer.APIKeyID(id)
return int(record.ID), record

View File

@@ -31,7 +31,7 @@ func (service BaseDataServiceTx[T, I]) Read(ID I) (*T, error) {
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
var collection = make([]T, 0)
return collection, service.Tx.GetAllWithJsoniter(
return collection, service.Tx.GetAll(
service.Bucket,
new(T),
AppendFn(&collection),

View File

@@ -19,7 +19,7 @@ func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFun
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
group.ID = portainer.EdgeGroupID(id)
return int(group.ID), group
},

View File

@@ -15,7 +15,7 @@ type Service struct {
connection portainer.Connection
idxVersion map[portainer.EdgeStackID]int
mu sync.RWMutex
cacheInvalidationFn func(portainer.EdgeStackID)
cacheInvalidationFn func(portainer.Transaction, portainer.EdgeStackID)
}
func (service *Service) BucketName() string {
@@ -23,7 +23,7 @@ func (service *Service) BucketName() string {
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection, cacheInvalidationFn func(portainer.EdgeStackID)) (*Service, error) {
func NewService(connection portainer.Connection, cacheInvalidationFn func(portainer.Transaction, portainer.EdgeStackID)) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
@@ -36,7 +36,7 @@ func NewService(connection portainer.Connection, cacheInvalidationFn func(portai
}
if s.cacheInvalidationFn == nil {
s.cacheInvalidationFn = func(portainer.EdgeStackID) {}
s.cacheInvalidationFn = func(portainer.Transaction, portainer.EdgeStackID) {}
}
es, err := s.EdgeStacks()
@@ -106,7 +106,7 @@ func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.Ed
service.mu.Lock()
service.idxVersion[id] = edgeStack.Version
service.cacheInvalidationFn(id)
service.cacheInvalidationFn(service.connection, id)
service.mu.Unlock()
return nil
@@ -125,7 +125,7 @@ func (service *Service) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *por
}
service.idxVersion[ID] = edgeStack.Version
service.cacheInvalidationFn(ID)
service.cacheInvalidationFn(service.connection, ID)
return nil
}
@@ -142,7 +142,7 @@ func (service *Service) UpdateEdgeStackFunc(ID portainer.EdgeStackID, updateFunc
updateFunc(edgeStack)
service.idxVersion[ID] = edgeStack.Version
service.cacheInvalidationFn(ID)
service.cacheInvalidationFn(service.connection, ID)
})
}
@@ -165,7 +165,7 @@ func (service *Service) DeleteEdgeStack(ID portainer.EdgeStackID) error {
delete(service.idxVersion, ID)
service.cacheInvalidationFn(ID)
service.cacheInvalidationFn(service.connection, ID)
return nil
}

View File

@@ -24,7 +24,7 @@ func (service ServiceTx) EdgeStacks() ([]portainer.EdgeStack, error) {
err := service.tx.GetAll(
BucketName,
&portainer.EdgeStack{},
func(obj interface{}) (interface{}, error) {
func(obj any) (any, error) {
stack, ok := obj.(*portainer.EdgeStack)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
@@ -44,8 +44,7 @@ func (service ServiceTx) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeSta
var stack portainer.EdgeStack
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &stack)
if err != nil {
if err := service.tx.GetObject(BucketName, identifier, &stack); err != nil {
return nil, err
}
@@ -65,18 +64,17 @@ func (service ServiceTx) EdgeStackVersion(ID portainer.EdgeStackID) (int, bool)
func (service ServiceTx) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
edgeStack.ID = id
err := service.tx.CreateObjectWithId(
if err := service.tx.CreateObjectWithId(
BucketName,
int(edgeStack.ID),
edgeStack,
)
if err != nil {
); err != nil {
return err
}
service.service.mu.Lock()
service.service.idxVersion[id] = edgeStack.Version
service.service.cacheInvalidationFn(id)
service.service.cacheInvalidationFn(service.tx, id)
service.service.mu.Unlock()
return nil
@@ -89,13 +87,12 @@ func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *po
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.UpdateObject(BucketName, identifier, edgeStack)
if err != nil {
if err := service.tx.UpdateObject(BucketName, identifier, edgeStack); err != nil {
return err
}
service.service.idxVersion[ID] = edgeStack.Version
service.service.cacheInvalidationFn(ID)
service.service.cacheInvalidationFn(service.tx, ID)
return nil
}
@@ -119,14 +116,13 @@ func (service ServiceTx) DeleteEdgeStack(ID portainer.EdgeStackID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.DeleteObject(BucketName, identifier)
if err != nil {
if err := service.tx.DeleteObject(BucketName, identifier); err != nil {
return err
}
delete(service.service.idxVersion, ID)
service.service.cacheInvalidationFn(ID)
service.service.cacheInvalidationFn(service.tx, ID)
return nil
}

View File

@@ -6,6 +6,8 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
// BucketName represents the name of the bucket where this service stores data.
@@ -157,6 +159,7 @@ func (service *Service) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.
return true
}
}
return false
}),
)
@@ -166,11 +169,13 @@ func (service *Service) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.
func (service *Service) GetNextIdentifier() int {
var identifier int
service.connection.UpdateTx(func(tx portainer.Transaction) error {
if err := service.connection.UpdateTx(func(tx portainer.Transaction) error {
identifier = service.Tx(tx).GetNextIdentifier()
return nil
})
}); err != nil {
log.Error().Err(err).Str("bucket", BucketName).Msg("could not get the next identifier")
}
return identifier
}

View File

@@ -20,10 +20,10 @@ func (service ServiceTx) BucketName() string {
// 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 {
if err := service.tx.GetObject(BucketName, identifier, &endpoint); err != nil {
return nil, err
}
@@ -36,8 +36,7 @@ func (service ServiceTx) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint,
func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error {
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.UpdateObject(BucketName, identifier, endpoint)
if err != nil {
if err := service.tx.UpdateObject(BucketName, identifier, endpoint); err != nil {
return err
}
@@ -45,6 +44,7 @@ func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *porta
if len(endpoint.EdgeID) > 0 {
service.service.idxEdgeID[endpoint.EdgeID] = ID
}
service.service.heartbeats.Store(ID, endpoint.LastCheckInDate)
service.service.mu.Unlock()
@@ -57,8 +57,7 @@ func (service ServiceTx) UpdateEndpoint(ID portainer.EndpointID, endpoint *porta
func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.DeleteObject(BucketName, identifier)
if err != nil {
if err := service.tx.DeleteObject(BucketName, identifier); err != nil {
return err
}
@@ -70,6 +69,7 @@ func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
break
}
}
service.service.heartbeats.Delete(ID)
service.service.mu.Unlock()
@@ -82,7 +82,7 @@ func (service ServiceTx) DeleteEndpoint(ID portainer.EndpointID) error {
func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
return endpoints, service.tx.GetAllWithJsoniter(
return endpoints, service.tx.GetAll(
BucketName,
&portainer.Endpoint{},
dataservices.AppendFn(&endpoints),
@@ -107,8 +107,7 @@ func (service ServiceTx) UpdateHeartbeat(endpointID portainer.EndpointID) {
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint)
if err != nil {
if err := service.tx.CreateObjectWithId(BucketName, int(endpoint.ID), endpoint); err != nil {
return err
}
@@ -116,6 +115,7 @@ func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
if len(endpoint.EdgeID) > 0 {
service.service.idxEdgeID[endpoint.EdgeID] = endpoint.ID
}
service.service.heartbeats.Store(endpoint.ID, endpoint.LastCheckInDate)
service.service.mu.Unlock()
@@ -134,6 +134,7 @@ func (service ServiceTx) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer
return true
}
}
return false
}),
)

View File

@@ -41,7 +41,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
func (service *Service) Create(endpointGroup *portainer.EndpointGroup) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
endpointGroup.ID = portainer.EndpointGroupID(id)
return int(endpointGroup.ID), endpointGroup
},

View File

@@ -13,7 +13,7 @@ type ServiceTx struct {
func (service ServiceTx) Create(endpointGroup *portainer.EndpointGroup) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
endpointGroup.ID = portainer.EndpointGroupID(id)
return int(endpointGroup.ID), endpointGroup
},

View File

@@ -1,6 +1,8 @@
package endpointrelation
import (
"sync"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/edge/cache"
@@ -13,11 +15,15 @@ const BucketName = "endpoint_relations"
// Service represents a service for managing environment(endpoint) relation data.
type Service struct {
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
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
endpointRelationsCache []portainer.EndpointRelation
mu sync.Mutex
}
var _ dataservices.EndpointRelationService = &Service{}
func (service *Service) BucketName() string {
return BucketName
}
@@ -32,8 +38,7 @@ func (service *Service) RegisterUpdateStackFunction(
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
if err := connection.SetServiceName(BucketName); err != nil {
return nil, err
}
@@ -65,8 +70,7 @@ func (service *Service) EndpointRelation(endpointID portainer.EndpointID) (*port
var endpointRelation portainer.EndpointRelation
identifier := service.connection.ConvertToKey(int(endpointID))
err := service.connection.GetObject(BucketName, identifier, &endpointRelation)
if err != nil {
if err := service.connection.GetObject(BucketName, identifier, &endpointRelation); err != nil {
return nil, err
}
@@ -78,6 +82,10 @@ func (service *Service) Create(endpointRelation *portainer.EndpointRelation) err
err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
cache.Del(endpointRelation.EndpointID)
service.mu.Lock()
service.endpointRelationsCache = nil
service.mu.Unlock()
return err
}
@@ -94,11 +102,27 @@ func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID,
updatedRelationState, _ := service.EndpointRelation(endpointID)
service.mu.Lock()
service.endpointRelationsCache = nil
service.mu.Unlock()
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
return nil
}
func (service *Service) AddEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
return service.connection.ViewTx(func(tx portainer.Transaction) error {
return service.Tx(tx).AddEndpointRelationsForEdgeStack(endpointIDs, edgeStackID)
})
}
func (service *Service) RemoveEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
return service.connection.ViewTx(func(tx portainer.Transaction) error {
return service.Tx(tx).RemoveEndpointRelationsForEdgeStack(endpointIDs, edgeStackID)
})
}
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
deletedRelation, _ := service.EndpointRelation(endpointID)
@@ -110,27 +134,15 @@ func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID)
return err
}
service.mu.Lock()
service.endpointRelationsCache = nil
service.mu.Unlock()
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()
@@ -161,19 +173,24 @@ func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationStat
// 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
}
if !refStackEnabled {
continue
}
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
})
if err := service.updateStackFn(refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
}); err != nil {
log.Error().Err(err).Msg("could not update the number of deployments")
}
}
}

View File

@@ -13,6 +13,8 @@ type ServiceTx struct {
tx portainer.Transaction
}
var _ dataservices.EndpointRelationService = &ServiceTx{}
func (service ServiceTx) BucketName() string {
return BucketName
}
@@ -33,8 +35,7 @@ func (service ServiceTx) EndpointRelation(endpointID portainer.EndpointID) (*por
var endpointRelation portainer.EndpointRelation
identifier := service.service.connection.ConvertToKey(int(endpointID))
err := service.tx.GetObject(BucketName, identifier, &endpointRelation)
if err != nil {
if err := service.tx.GetObject(BucketName, identifier, &endpointRelation); err != nil {
return nil, err
}
@@ -46,6 +47,10 @@ func (service ServiceTx) Create(endpointRelation *portainer.EndpointRelation) er
err := service.tx.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
cache.Del(endpointRelation.EndpointID)
service.service.mu.Lock()
service.service.endpointRelationsCache = nil
service.service.mu.Unlock()
return err
}
@@ -62,11 +67,67 @@ func (service ServiceTx) UpdateEndpointRelation(endpointID portainer.EndpointID,
updatedRelationState, _ := service.EndpointRelation(endpointID)
service.service.mu.Lock()
service.service.endpointRelationsCache = nil
service.service.mu.Unlock()
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
return nil
}
func (service ServiceTx) AddEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
for _, endpointID := range endpointIDs {
rel, err := service.EndpointRelation(endpointID)
if err != nil {
return err
}
rel.EdgeStacks[edgeStackID] = true
identifier := service.service.connection.ConvertToKey(int(endpointID))
err = service.tx.UpdateObject(BucketName, identifier, rel)
cache.Del(endpointID)
if err != nil {
return err
}
}
if err := service.service.updateStackFnTx(service.tx, edgeStackID, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments += len(endpointIDs)
}); err != nil {
log.Error().Err(err).Msg("could not update the number of deployments")
}
return nil
}
func (service ServiceTx) RemoveEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error {
for _, endpointID := range endpointIDs {
rel, err := service.EndpointRelation(endpointID)
if err != nil {
return err
}
delete(rel.EdgeStacks, edgeStackID)
identifier := service.service.connection.ConvertToKey(int(endpointID))
err = service.tx.UpdateObject(BucketName, identifier, rel)
cache.Del(endpointID)
if err != nil {
return err
}
}
if err := service.service.updateStackFnTx(service.tx, edgeStackID, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments -= len(endpointIDs)
}); err != nil {
log.Error().Err(err).Msg("could not update the number of deployments")
}
return nil
}
// DeleteEndpointRelation deletes an Environment(Endpoint) relation object
func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
deletedRelation, _ := service.EndpointRelation(endpointID)
@@ -78,27 +139,44 @@ func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID)
return err
}
service.service.mu.Lock()
service.service.endpointRelationsCache = nil
service.service.mu.Unlock()
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
return nil
}
func (service ServiceTx) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
rels, err := service.EndpointRelations()
rels, err := service.cachedEndpointRelations()
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)
}
if _, ok := rel.EdgeStacks[edgeStackID]; ok {
cache.Del(rel.EndpointID)
}
}
}
func (service ServiceTx) cachedEndpointRelations() ([]portainer.EndpointRelation, error) {
service.service.mu.Lock()
defer service.service.mu.Unlock()
if service.service.endpointRelationsCache == nil {
var err error
service.service.endpointRelationsCache, err = service.EndpointRelations()
if err != nil {
return nil, err
}
}
return service.service.endpointRelationsCache, nil
}
func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
relations, _ := service.EndpointRelations()
@@ -129,19 +207,24 @@ func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationSta
// 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
}
if !refStackEnabled {
continue
}
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
})
if err := service.service.updateStackFnTx(service.tx, refStackId, func(edgeStack *portainer.EdgeStack) {
edgeStack.NumDeployments = numDeployments
}); err != nil {
log.Error().Err(err).Msg("could not update the number of deployments")
}
}
}

View File

@@ -1,43 +0,0 @@
package fdoprofile
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
// BucketName represents the name of the bucket where this service stores data.
const BucketName = "fdo_profiles"
// Service represents a service for managingFDO Profiles data.
type Service struct {
dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
BaseDataService: dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]{
Bucket: BucketName,
Connection: connection,
},
}, nil
}
// Create assign an ID to a new FDO Profile and saves it.
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
return service.Connection.CreateObjectWithId(
BucketName,
int(FDOProfile.ID),
FDOProfile,
)
}
// GetNextIdentifier returns the next identifier for a FDO Profile.
func (service *Service) GetNextIdentifier() int {
return service.Connection.GetNextIdentifier(BucketName)
}

View File

@@ -45,7 +45,7 @@ func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]p
func (service *Service) Create(record *portainer.HelmUserRepository) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
record.ID = portainer.HelmUserRepositoryID(id)
return int(record.ID), record
},

View File

@@ -17,8 +17,8 @@ func IsErrObjectNotFound(e error) bool {
}
// AppendFn appends elements to the given collection slice
func AppendFn[T any](collection *[]T) func(obj interface{}) (interface{}, error) {
return func(obj interface{}) (interface{}, error) {
func AppendFn[T any](collection *[]T) func(obj any) (any, error) {
return func(obj any) (any, error) {
element, ok := obj.(*T)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
@@ -32,8 +32,8 @@ func AppendFn[T any](collection *[]T) func(obj interface{}) (interface{}, error)
}
// FilterFn appends elements to the given collection when the predicate is true
func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj interface{}) (interface{}, error) {
return func(obj interface{}) (interface{}, error) {
func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj any) (any, error) {
return func(obj any) (any, error) {
element, ok := obj.(*T)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")
@@ -50,8 +50,8 @@ func FilterFn[T any](collection *[]T, predicate func(T) bool) func(obj interface
// FirstFn sets the element to the first one that satisfies the predicate and stops the computation, returns ErrStop on
// success
func FirstFn[T any](element *T, predicate func(T) bool) func(obj interface{}) (interface{}, error) {
return func(obj interface{}) (interface{}, error) {
func FirstFn[T any](element *T, predicate func(T) bool) func(obj any) (any, error) {
return func(obj any) (any, error) {
e, ok := obj.(*T)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("type assertion failed")

View File

@@ -15,7 +15,6 @@ type (
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
FDOProfile() FDOProfileService
HelmUserRepository() HelmUserRepositoryService
Registry() RegistryService
ResourceControl() ResourceControlService
@@ -36,6 +35,7 @@ type (
}
DataStore interface {
Connection() portainer.Connection
Open() (newStore bool, err error)
Init() error
Close() error
@@ -71,8 +71,9 @@ type (
}
PendingActionsService interface {
BaseCRUD[portainer.PendingActions, portainer.PendingActionsID]
BaseCRUD[portainer.PendingAction, portainer.PendingActionID]
GetNextIdentifier() int
DeleteByEndpointID(ID portainer.EndpointID) error
}
// EdgeStackService represents a service to manage Edge stacks
@@ -114,16 +115,12 @@ type (
EndpointRelation(EndpointID portainer.EndpointID) (*portainer.EndpointRelation, error)
Create(endpointRelation *portainer.EndpointRelation) error
UpdateEndpointRelation(EndpointID portainer.EndpointID, endpointRelation *portainer.EndpointRelation) error
AddEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error
RemoveEndpointRelationsForEdgeStack(endpointIDs []portainer.EndpointID, edgeStackID portainer.EdgeStackID) error
DeleteEndpointRelation(EndpointID portainer.EndpointID) error
BucketName() string
}
// FDOProfileService represents a service to manage FDO Profiles
FDOProfileService interface {
BaseCRUD[portainer.FDOProfile, portainer.FDOProfileID]
GetNextIdentifier() int
}
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
HelmUserRepositoryService interface {
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]

View File

@@ -1,10 +1,12 @@
package pendingactions
import (
"fmt"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
const (
@@ -12,11 +14,11 @@ const (
)
type Service struct {
dataservices.BaseDataService[portainer.PendingActions, portainer.PendingActionsID]
dataservices.BaseDataService[portainer.PendingAction, portainer.PendingActionID]
}
type ServiceTx struct {
dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]
dataservices.BaseDataServiceTx[portainer.PendingAction, portainer.PendingActionID]
}
func NewService(connection portainer.Connection) (*Service, error) {
@@ -26,28 +28,34 @@ func NewService(connection portainer.Connection) (*Service, error) {
}
return &Service{
BaseDataService: dataservices.BaseDataService[portainer.PendingActions, portainer.PendingActionsID]{
BaseDataService: dataservices.BaseDataService[portainer.PendingAction, portainer.PendingActionID]{
Bucket: BucketName,
Connection: connection,
},
}, nil
}
func (s Service) Create(config *portainer.PendingActions) error {
func (s Service) Create(config *portainer.PendingAction) error {
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Create(config)
})
}
func (s Service) Update(ID portainer.PendingActionsID, config *portainer.PendingActions) error {
func (s Service) Update(ID portainer.PendingActionID, config *portainer.PendingAction) error {
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Update(ID, config)
})
}
func (s Service) DeleteByEndpointID(ID portainer.EndpointID) error {
return s.Connection.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).DeleteByEndpointID(ID)
})
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.PendingActions, portainer.PendingActionsID]{
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.PendingAction, portainer.PendingActionID]{
Bucket: BucketName,
Connection: service.Connection,
Tx: tx,
@@ -55,19 +63,42 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
}
}
func (s ServiceTx) Create(config *portainer.PendingActions) error {
return s.Tx.CreateObject(BucketName, func(id uint64) (int, interface{}) {
config.ID = portainer.PendingActionsID(id)
func (s ServiceTx) Create(config *portainer.PendingAction) error {
return s.Tx.CreateObject(BucketName, func(id uint64) (int, any) {
config.ID = portainer.PendingActionID(id)
config.CreatedAt = time.Now().Unix()
return int(config.ID), config
})
}
func (s ServiceTx) Update(ID portainer.PendingActionsID, config *portainer.PendingActions) error {
func (s ServiceTx) Update(ID portainer.PendingActionID, config *portainer.PendingAction) error {
return s.BaseDataServiceTx.Update(ID, config)
}
func (s ServiceTx) DeleteByEndpointID(ID portainer.EndpointID) error {
log.Debug().Int("endpointId", int(ID)).Msg("deleting pending actions for endpoint")
pendingActions, err := s.BaseDataServiceTx.ReadAll()
if err != nil {
return fmt.Errorf("failed to retrieve pending-actions for endpoint (%d): %w", ID, err)
}
for _, pendingAction := range pendingActions {
if pendingAction.EndpointID == ID {
err := s.BaseDataServiceTx.Delete(pendingAction.ID)
if err != nil {
log.Debug().Int("endpointId", int(ID)).Msgf("failed to delete pending action: %v", err)
}
}
}
return nil
}
// GetNextIdentifier returns the next identifier for a custom template.
func (service ServiceTx) GetNextIdentifier() int {
return service.Tx.GetNextIdentifier(BucketName)
}
// GetNextIdentifier returns the next identifier for a custom template.
func (service *Service) GetNextIdentifier() int {
return service.Connection.GetNextIdentifier(BucketName)

View File

@@ -42,7 +42,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
func (service *Service) Create(registry *portainer.Registry) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
registry.ID = portainer.RegistryID(id)
return int(registry.ID), registry
},

View File

@@ -13,7 +13,7 @@ type ServiceTx struct {
func (service ServiceTx) Create(registry *portainer.Registry) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
registry.ID = portainer.RegistryID(id)
return int(registry.ID), registry
},

View File

@@ -48,11 +48,11 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
// if no ResourceControl was found.
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
stop := fmt.Errorf("ok")
stop := errors.New("ok")
err := service.Connection.GetAll(
BucketName,
&portainer.ResourceControl{},
func(obj interface{}) (interface{}, error) {
func(obj any) (any, error) {
rc, ok := obj.(*portainer.ResourceControl)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
@@ -84,7 +84,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
func (service *Service) Create(resourceControl *portainer.ResourceControl) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
resourceControl.ID = portainer.ResourceControlID(id)
return int(resourceControl.ID), resourceControl
},

View File

@@ -19,11 +19,11 @@ type ServiceTx struct {
// if no ResourceControl was found.
func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl
stop := fmt.Errorf("ok")
stop := errors.New("ok")
err := service.Tx.GetAll(
BucketName,
&portainer.ResourceControl{},
func(obj interface{}) (interface{}, error) {
func(obj any) (any, error) {
rc, ok := obj.(*portainer.ResourceControl)
if !ok {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
@@ -55,7 +55,7 @@ func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, r
func (service ServiceTx) Create(resourceControl *portainer.ResourceControl) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
resourceControl.ID = portainer.ResourceControlID(id)
return int(resourceControl.ID), resourceControl
},

View File

@@ -42,7 +42,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
func (service *Service) Create(role *portainer.Role) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
role.ID = portainer.RoleID(id)
return int(role.ID), role
},

View File

@@ -13,7 +13,7 @@ type ServiceTx struct {
func (service ServiceTx) Create(role *portainer.Role) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
role.ID = portainer.RoleID(id)
return int(role.ID), role
},

View File

@@ -33,7 +33,7 @@ func TestService_StackByWebhookID(t *testing.T) {
b := stackBuilder{t: t, store: store}
b.createNewStack(newGuidString(t))
for i := 0; i < 10; i++ {
for range 10 {
b.createNewStack("")
}
webhookID := newGuidString(t)

View File

@@ -42,7 +42,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
func (service *Service) Create(tag *portainer.Tag) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
tag.ID = portainer.TagID(id)
return int(tag.ID), tag
},

View File

@@ -15,7 +15,7 @@ type ServiceTx struct {
func (service ServiceTx) Create(tag *portainer.Tag) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
tag.ID = portainer.TagID(id)
return int(tag.ID), tag
},

View File

@@ -19,8 +19,7 @@ type Service struct {
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
if err := connection.SetServiceName(BucketName); err != nil {
return nil, err
}
@@ -32,6 +31,16 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
BaseDataServiceTx: dataservices.BaseDataServiceTx[portainer.Team, portainer.TeamID]{
Bucket: BucketName,
Connection: service.Connection,
Tx: tx,
},
}
}
// TeamByName returns a team by name.
func (service *Service) TeamByName(name string) (*portainer.Team, error) {
var t portainer.Team
@@ -59,7 +68,7 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
func (service *Service) Create(team *portainer.Team) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
team.ID = portainer.TeamID(id)
return int(team.ID), team
},

View File

@@ -0,0 +1,48 @@
package team
import (
"errors"
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
)
type ServiceTx struct {
dataservices.BaseDataServiceTx[portainer.Team, portainer.TeamID]
}
// TeamByName returns a team by name.
func (service ServiceTx) TeamByName(name string) (*portainer.Team, error) {
var t portainer.Team
err := service.Tx.GetAll(
BucketName,
&portainer.Team{},
dataservices.FirstFn(&t, func(e portainer.Team) bool {
return strings.EqualFold(e.Name, name)
}),
)
if errors.Is(err, dataservices.ErrStop) {
return &t, nil
}
if err == nil {
return nil, dserrors.ErrObjectNotFound
}
return nil, err
}
// CreateTeam creates a new Team.
func (service ServiceTx) Create(team *portainer.Team) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, any) {
team.ID = portainer.TeamID(id)
return int(team.ID), team
},
)
}

View File

@@ -72,7 +72,7 @@ func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]port
func (service *Service) Create(membership *portainer.TeamMembership) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
membership.ID = portainer.TeamMembershipID(id)
return int(membership.ID), membership
},
@@ -84,8 +84,8 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
return service.Connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
func(obj any) (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)
@@ -105,8 +105,8 @@ func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) er
return service.Connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
func(obj any) (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)
@@ -125,8 +125,8 @@ func (service *Service) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.T
return service.Connection.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
func(obj any) (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)

View File

@@ -43,7 +43,7 @@ func (service ServiceTx) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]por
func (service ServiceTx) Create(membership *portainer.TeamMembership) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
membership.ID = portainer.TeamMembershipID(id)
return int(membership.ID), membership
},
@@ -55,7 +55,7 @@ func (service ServiceTx) DeleteTeamMembershipByUserID(userID portainer.UserID) e
return service.Tx.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
func(obj any) (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")
@@ -76,7 +76,7 @@ func (service ServiceTx) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) e
return service.Tx.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
func(obj any) (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")
@@ -96,7 +96,7 @@ func (service ServiceTx) DeleteTeamMembershipByTeamIDAndUserID(teamID portainer.
return service.Tx.DeleteAllObjects(
BucketName,
&portainer.TeamMembership{},
func(obj interface{}) (id int, ok bool) {
func(obj any) (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")

View File

@@ -53,7 +53,7 @@ func (service ServiceTx) UsersByRole(role portainer.UserRole) ([]portainer.User,
func (service ServiceTx) Create(user *portainer.User) error {
return service.Tx.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
user.ID = portainer.UserID(id)
user.Username = strings.ToLower(user.Username)

View File

@@ -82,7 +82,7 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
func (service *Service) Create(user *portainer.User) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
user.ID = portainer.UserID(id)
user.Username = strings.ToLower(user.Username)

View File

@@ -81,7 +81,7 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
func (service *Service) Create(webhook *portainer.Webhook) error {
return service.Connection.CreateObject(
BucketName,
func(id uint64) (int, interface{}) {
func(id uint64) (int, any) {
webhook.ID = portainer.WebhookID(id)
return int(webhook.ID), webhook
},

View File

@@ -1,7 +1,6 @@
package datastore
import (
"fmt"
"testing"
portainer "github.com/portainer/portainer/api"
@@ -33,7 +32,7 @@ func TestStoreCreation(t *testing.T) {
func TestBackup(t *testing.T) {
_, store := MustNewTestStore(t, true, true)
backupFileName := store.backupFilename()
t.Run(fmt.Sprintf("Backup should create %s", backupFileName), func(t *testing.T) {
t.Run("Backup should create "+backupFileName, func(t *testing.T) {
v := models.Version{
Edition: int(portainer.PortainerCE),
SchemaVersion: portainer.APIVersion,

View File

@@ -16,8 +16,9 @@ import (
)
// NewStore initializes a new Store and the associated services
func NewStore(storePath string, fileService portainer.FileService, connection portainer.Connection) *Store {
func NewStore(cliFlags *portainer.CLIFlags, fileService portainer.FileService, connection portainer.Connection) *Store {
return &Store{
flags: cliFlags,
fileService: fileService,
connection: connection,
}

View File

@@ -57,7 +57,7 @@ func (store *Store) checkOrCreateDefaultSettings() error {
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
KubectlShellImage: portainer.DefaultKubectlShellImage,
KubectlShellImage: *store.flags.KubectlShellImage,
IsDockerDesktopExtension: isDDExtention,
}

View File

@@ -32,7 +32,7 @@ func (store *Store) MigrateData() error {
return errors.Wrap(err, "while migrating legacy version")
}
migratorParams := store.newMigratorParameters(version)
migratorParams := store.newMigratorParameters(version, store.flags)
migrator := migrator.NewMigrator(migratorParams)
if !migrator.NeedsMigration() {
@@ -62,8 +62,9 @@ func (store *Store) MigrateData() error {
return nil
}
func (store *Store) newMigratorParameters(version *models.Version) *migrator.MigratorParameters {
func (store *Store) newMigratorParameters(version *models.Version, flags *portainer.CLIFlags) *migrator.MigratorParameters {
return &migrator.MigratorParameters{
Flags: flags,
CurrentDBVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
@@ -86,6 +87,7 @@ func (store *Store) newMigratorParameters(version *models.Version) *migrator.Mig
EdgeStackService: store.EdgeStackService,
EdgeJobService: store.EdgeJobService,
TunnelServerService: store.TunnelServerService,
PendingActionsService: store.PendingActionsService,
}
}

View File

@@ -109,7 +109,7 @@ func TestMigrateData(t *testing.T) {
t.FailNow()
}
migratorParams := store.newMigratorParameters(v)
migratorParams := store.newMigratorParameters(v, store.flags)
m := migrator.NewMigrator(migratorParams)
latestMigrations := m.LatestMigrations()
@@ -321,7 +321,7 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanc
// importJSON reads input JSON and commits it to a portainer datastore.Store.
// Errors are logged with the testing package.
func importJSON(t *testing.T, r io.Reader, store *Store) error {
objects := make(map[string]interface{})
objects := make(map[string]any)
// Parse json into map of objects.
d := json.NewDecoder(r)
@@ -337,9 +337,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
for k, v := range objects {
switch k {
case "version":
versions, ok := v.(map[string]interface{})
versions, ok := v.(map[string]any)
if !ok {
t.Logf("failed casting %s to map[string]interface{}", k)
t.Logf("failed casting %s to map[string]any", k)
}
// New format db
@@ -404,9 +404,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
}
case "dockerhub":
obj, ok := v.([]interface{})
obj, ok := v.([]any)
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
t.Logf("failed to cast %s to []any", k)
}
err := con.CreateObjectWithStringId(
k,
@@ -418,9 +418,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
}
case "ssl":
obj, ok := v.(map[string]interface{})
obj, ok := v.(map[string]any)
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
t.Logf("failed to case %s to map[string]any", k)
}
err := con.CreateObjectWithStringId(
k,
@@ -432,9 +432,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
}
case "settings":
obj, ok := v.(map[string]interface{})
obj, ok := v.(map[string]any)
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
t.Logf("failed to case %s to map[string]any", k)
}
err := con.CreateObjectWithStringId(
k,
@@ -446,9 +446,9 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
}
case "tunnel_server":
obj, ok := v.(map[string]interface{})
obj, ok := v.(map[string]any)
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
t.Logf("failed to case %s to map[string]any", k)
}
err := con.CreateObjectWithStringId(
k,
@@ -462,18 +462,18 @@ func importJSON(t *testing.T, r io.Reader, store *Store) error {
continue
default:
objlist, ok := v.([]interface{})
objlist, ok := v.([]any)
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
t.Logf("failed to cast %s to []any", k)
}
for _, obj := range objlist {
value, ok := obj.(map[string]interface{})
value, ok := obj.(map[string]any)
if !ok {
t.Logf("failed to cast %v to map[string]interface{}", obj)
t.Logf("failed to cast %v to map[string]any", obj)
} else {
var ok bool
var id interface{}
var id any
switch k {
case "endpoint_relations":
// TODO: need to make into an int, then do that weird

View File

@@ -12,13 +12,13 @@ const dummyLogoURL = "example.com"
// initTestingDBConn creates a settings service with raw database DB connection
// for unit testing usage only since using NewStore will cause cycle import inside migrator pkg
func initTestingSettingsService(dbConn portainer.Connection, preSetObj map[string]interface{}) error {
func initTestingSettingsService(dbConn portainer.Connection, preSetObj map[string]any) error {
//insert a obj
return dbConn.UpdateObject("settings", []byte("SETTINGS"), preSetObj)
}
func setup(store *Store) error {
dummySettingsObj := map[string]interface{}{
dummySettingsObj := map[string]any{
"LogoURL": dummyLogoURL,
}
@@ -48,6 +48,7 @@ func TestMigrateSettings(t *testing.T) {
}
m := migrator.NewMigrator(&migrator.MigratorParameters{
Flags: store.flags,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,

View File

@@ -99,7 +99,7 @@ func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) {
return &models.Version{
SchemaVersion: dbVersionToSemanticVersion(dbVersion),
Edition: edition,
InstanceID: string(instanceId),
InstanceID: instanceId,
}, nil
}
@@ -111,5 +111,6 @@ func (store *Store) finishMigrateLegacyVersion(versionToWrite *models.Version) e
store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey))
store.connection.DeleteObject(bucketName, []byte(legacyEditionKey))
store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
return err
}

View File

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

View File

@@ -15,7 +15,7 @@ func migrationError(err error, context string) error {
return errors.Wrap(err, "failed in "+context)
}
func GetFunctionName(i interface{}) string {
func GetFunctionName(i any) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
@@ -39,20 +39,19 @@ func (m *Migrator) Migrate() error {
latestMigrations := m.LatestMigrations()
if latestMigrations.Version.Equal(schemaVersion) &&
version.MigratorCount != len(latestMigrations.MigrationFuncs) {
err := runMigrations(latestMigrations.MigrationFuncs)
if err != nil {
if err := runMigrations(latestMigrations.MigrationFuncs); 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) {
log.Info().Msgf("migrating data to %s", migration.Version.String())
err := runMigrations(migration.MigrationFuncs)
if err != nil {
if err := runMigrations(migration.MigrationFuncs); err != nil {
return err
}
}
@@ -63,16 +62,14 @@ func (m *Migrator) Migrate() error {
}
}
err = m.Always()
if err != nil {
if err := m.Always(); err != nil {
return migrationError(err, "Always migrations returned error")
}
version.SchemaVersion = portainer.APIVersion
version.MigratorCount = newMigratorCount
err = m.versionService.UpdateVersion(version)
if err != nil {
if err := m.versionService.UpdateVersion(version); err != nil {
return migrationError(err, "StoreDBVersion")
}
@@ -99,6 +96,7 @@ func (m *Migrator) NeedsMigration() bool {
// 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
}

View File

@@ -7,6 +7,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/chisel/crypto"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
@@ -37,9 +38,11 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
log.Info().Msg("ServerInfo object not found")
return nil
}
log.Error().
Err(err).
Msg("Failed to read ServerInfo from DB")
return err
}
@@ -49,14 +52,15 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
log.Error().
Err(err).
Msg("Failed to read ServerInfo from DB")
return err
}
err = m.fileService.StoreChiselPrivateKey(key)
if err != nil {
if err := m.fileService.StoreChiselPrivateKey(key); err != nil {
log.Error().
Err(err).
Msg("Failed to save Chisel private key to disk")
return err
}
} else {
@@ -64,14 +68,14 @@ func (m *Migrator) convertSeedToPrivateKeyForDB100() error {
}
serverInfo.PrivateKeySeed = ""
err = m.TunnelServerService.UpdateInfo(serverInfo)
if err != nil {
if err := m.TunnelServerService.UpdateInfo(serverInfo); err != nil {
log.Error().
Err(err).
Msg("Failed to clean private key seed in DB")
} else {
log.Info().Msg("Success to migrate private key seed to private key file")
}
return err
}
@@ -84,13 +88,16 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
}
for _, edgeStack := range edgeStacks {
for environmentID, environmentStatus := range edgeStack.Status {
// skip if status is already updated
// Skip if status is already updated
if len(environmentStatus.Status) > 0 {
continue
}
if environmentStatus.Details == nil {
continue
}
statusArray := []portainer.EdgeStackDeploymentStatus{}
if environmentStatus.Details.Pending {
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{
@@ -146,8 +153,7 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
edgeStack.Status[environmentID] = environmentStatus
}
err = m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack)
if err != nil {
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
return err
}
}

View File

@@ -0,0 +1,32 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
func (migrator *Migrator) cleanPendingActionsForDeletedEndpointsForDB111() error {
log.Info().Msg("cleaning up pending actions for deleted endpoints")
pendingActions, err := migrator.pendingActionsService.ReadAll()
if err != nil {
return err
}
endpoints := make(map[portainer.EndpointID]struct{})
for _, action := range pendingActions {
endpoints[action.EndpointID] = struct{}{}
}
for endpointId := range endpoints {
_, err := migrator.endpointService.Endpoint(endpointId)
if dataservices.IsErrObjectNotFound(err) {
err := migrator.pendingActionsService.DeleteByEndpointID(endpointId)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,33 @@
package migrator
import (
"github.com/segmentio/encoding/json"
"github.com/rs/zerolog/log"
)
func (migrator *Migrator) migratePendingActionsDataForDB130() error {
log.Info().Msg("Migrating pending actions data")
pendingActions, err := migrator.pendingActionsService.ReadAll()
if err != nil {
return err
}
for _, pa := range pendingActions {
actionData, err := json.Marshal(pa.ActionData)
if err != nil {
return err
}
pa.ActionData = string(actionData)
// Update the pending action
err = migrator.pendingActionsService.Update(pa.ID, &pa)
if err != nil {
return err
}
}
return nil
}

View File

@@ -32,8 +32,8 @@ func (m *Migrator) updateStacksToDB24() error {
for idx := range stacks {
stack := &stacks[idx]
stack.Status = portainer.StackStatusActive
err := m.stackService.Update(stack.ID, stack)
if err != nil {
if err := m.stackService.Update(stack.ID, stack); err != nil {
return err
}
}

View File

@@ -123,7 +123,7 @@ func (m *Migrator) updateDockerhubToDB32() error {
migrated = true
} else {
// delete subsequent duplicates
m.registryService.Delete(portainer.RegistryID(r.ID))
m.registryService.Delete(r.ID)
}
}
}

View File

@@ -1,8 +1,6 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
@@ -20,7 +18,7 @@ func (m *Migrator) migrateSettingsToDB33() error {
}
log.Info().Msg("setting default kubectl shell image")
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
settings.KubectlShellImage = *m.flags.KubectlShellImage
return m.settingsService.UpdateSettings(settings)
}

View File

@@ -75,6 +75,10 @@ func (m *Migrator) updateEdgeStackStatusForDB80() error {
for _, edgeStack := range edgeStacks {
for endpointId, status := range edgeStack.Status {
if status.Details == nil {
status.Details = &portainer.EdgeStackStatusDetails{}
}
switch status.Type {
case portainer.EdgeStackStatusPending:
status.Details.Pending = true
@@ -93,10 +97,10 @@ func (m *Migrator) updateEdgeStackStatusForDB80() error {
edgeStack.Status[endpointId] = status
}
err = m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack)
if err != nil {
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
return err
}
}
return nil
}

View File

@@ -13,7 +13,7 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/pendingactions"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
"github.com/portainer/portainer/api/dataservices/role"
@@ -33,6 +33,7 @@ import (
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
flags *portainer.CLIFlags
currentDBVersion *models.Version
migrations []Migrations
@@ -40,7 +41,6 @@ type (
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
extensionService *extension.Service
fdoProfilesService *fdoprofile.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
@@ -58,16 +58,17 @@ type (
edgeStackService *edgestack.Service
edgeJobService *edgejob.Service
TunnelServerService *tunnelserver.Service
pendingActionsService *pendingactions.Service
}
// MigratorParameters represents the required parameters to create a new Migrator instance.
MigratorParameters struct {
Flags *portainer.CLIFlags
CurrentDBVersion *models.Version
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
@@ -85,18 +86,19 @@ type (
EdgeStackService *edgestack.Service
EdgeJobService *edgejob.Service
TunnelServerService *tunnelserver.Service
PendingActionsService *pendingactions.Service
}
)
// NewMigrator creates a new Migrator.
func NewMigrator(parameters *MigratorParameters) *Migrator {
migrator := &Migrator{
flags: parameters.Flags,
currentDBVersion: parameters.CurrentDBVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
extensionService: parameters.ExtensionService,
fdoProfilesService: parameters.FDOProfilesService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,
@@ -114,6 +116,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
edgeStackService: parameters.EdgeStackService,
edgeJobService: parameters.EdgeJobService,
TunnelServerService: parameters.TunnelServerService,
pendingActionsService: parameters.PendingActionsService,
}
migrator.initMigrations()
@@ -232,8 +235,14 @@ func (m *Migrator) initMigrations() {
m.updateAppTemplatesVersionForDB110,
m.updateResourceOverCommitToDB110,
)
m.addMigrations("2.20.2",
m.cleanPendingActionsForDeletedEndpointsForDB111,
)
m.addMigrations("2.22.0",
m.migratePendingActionsDataForDB130,
)
// Add new migrations below...
// Add new migrations above...
// One function per migration, each versions migration funcs in the same file.
}

View File

@@ -0,0 +1,98 @@
package datastore
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/api/pendingactions/handlers"
)
type cleanNAPWithOverridePolicies struct {
EndpointGroupID portainer.EndpointGroupID
}
func Test_ConvertCleanNAPWithOverridePoliciesPayload(t *testing.T) {
t.Run("test ConvertCleanNAPWithOverridePoliciesPayload", func(t *testing.T) {
_, store := MustNewTestStore(t, true, false)
defer store.Close()
gid := portainer.EndpointGroupID(1)
testData := []struct {
Name string
PendingAction portainer.PendingAction
Expected any
Err bool
}{
{
Name: "test actiondata with EndpointGroupID 1",
PendingAction: handlers.NewCleanNAPWithOverridePolicies(
1,
&gid,
),
Expected: portainer.EndpointGroupID(1),
},
{
Name: "test actionData nil",
PendingAction: handlers.NewCleanNAPWithOverridePolicies(
2,
nil,
),
Expected: nil,
},
{
Name: "test actionData empty and expected error",
PendingAction: portainer.PendingAction{
EndpointID: 2,
Action: actions.CleanNAPWithOverridePolicies,
ActionData: "",
},
Expected: nil,
Err: true,
},
}
for _, d := range testData {
err := store.PendingActions().Create(&d.PendingAction)
if err != nil {
t.Error(err)
return
}
pendingActions, err := store.PendingActions().ReadAll()
if err != nil {
t.Error(err)
return
}
for _, endpointPendingAction := range pendingActions {
t.Run(d.Name, func(t *testing.T) {
if endpointPendingAction.Action == actions.CleanNAPWithOverridePolicies {
var payload cleanNAPWithOverridePolicies
err := endpointPendingAction.UnmarshallActionData(&payload)
if d.Err && err == nil {
t.Error(err)
}
if d.Expected == nil && payload.EndpointGroupID != 0 {
t.Errorf("expected nil, got %d", payload.EndpointGroupID)
}
if d.Expected != nil {
expected := d.Expected.(portainer.EndpointGroupID)
if d.Expected != nil && expected != payload.EndpointGroupID {
t.Errorf("expected EndpointGroupID %d, got %d", expected, payload.EndpointGroupID)
}
}
}
})
}
store.PendingActions().Delete(d.PendingAction.ID)
}
})
}

View File

@@ -0,0 +1,197 @@
package postinit
import (
"context"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
dockerClient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/pkg/endpoints"
"github.com/rs/zerolog/log"
)
type PostInitMigrator struct {
kubeFactory *cli.ClientFactory
dockerFactory *dockerClient.ClientFactory
dataStore dataservices.DataStore
assetsPath string
kubernetesDeployer portainer.KubernetesDeployer
}
func NewPostInitMigrator(
kubeFactory *cli.ClientFactory,
dockerFactory *dockerClient.ClientFactory,
dataStore dataservices.DataStore,
assetsPath string,
kubernetesDeployer portainer.KubernetesDeployer,
) *PostInitMigrator {
return &PostInitMigrator{
kubeFactory: kubeFactory,
dockerFactory: dockerFactory,
dataStore: dataStore,
assetsPath: assetsPath,
kubernetesDeployer: kubernetesDeployer,
}
}
// PostInitMigrate will run all post-init migrations, which require docker/kube clients for all edge or non-edge environments
func (postInitMigrator *PostInitMigrator) PostInitMigrate() error {
environments, err := postInitMigrator.dataStore.Endpoint().Endpoints()
if err != nil {
log.Error().Err(err).Msg("Error getting environments")
return err
}
for _, environment := range environments {
// edge environments will run after the server starts, in pending actions
if endpoints.IsEdgeEndpoint(&environment) {
// Skip edge environments that do not have direct connectivity
if !endpoints.HasDirectConnectivity(&environment) {
continue
}
log.Info().
Int("endpoint_id", int(environment.ID)).
Msg("adding pending action 'PostInitMigrateEnvironment' for environment")
if err := postInitMigrator.createPostInitMigrationPendingAction(environment.ID); err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error creating pending action for environment")
}
} else {
// Non-edge environments will run before the server starts.
if err := postInitMigrator.MigrateEnvironment(&environment); err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error running post-init migrations for non-edge environment")
}
}
}
return nil
}
// try to create a post init migration pending action. If it already exists, do nothing
// this function exists for readability, not reusability
// TODO: This should be moved into pending actions as part of the pending action migration
func (postInitMigrator *PostInitMigrator) createPostInitMigrationPendingAction(environmentID portainer.EndpointID) error {
// If there are no pending actions for the given endpoint, create one
err := postInitMigrator.dataStore.PendingActions().Create(&portainer.PendingAction{
EndpointID: environmentID,
Action: actions.PostInitMigrateEnvironment,
})
if err != nil {
log.Error().Err(err).Msgf("Error creating pending action for environment %d", environmentID)
}
return nil
}
// MigrateEnvironment runs migrations on a single environment
func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endpoint) error {
log.Info().Msgf("Executing post init migration for environment %d", environment.ID)
switch {
case endpointutils.IsKubernetesEndpoint(environment):
// get the kubeclient for the environment, and skip all kube migrations if there's an error
kubeclient, err := migrator.kubeFactory.GetPrivilegedKubeClient(environment)
if err != nil {
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
return err
}
// if one environment fails, it is logged and the next migration runs. The error is returned at the end and handled by pending actions
err = migrator.MigrateIngresses(*environment, kubeclient)
if err != nil {
return err
}
return nil
case endpointutils.IsDockerEndpoint(environment):
// get the docker client for the environment, and skip all docker migrations if there's an error
dockerClient, err := migrator.dockerFactory.CreateClient(environment, "", nil)
if err != nil {
log.Error().Err(err).Msgf("Error creating docker client for environment: %d", environment.ID)
return err
}
defer dockerClient.Close()
migrator.MigrateGPUs(*environment, dockerClient)
}
return nil
}
func (migrator *PostInitMigrator) MigrateIngresses(environment portainer.Endpoint, kubeclient *cli.KubeClient) error {
// Early exit if we do not need to migrate!
if !environment.PostInitMigrations.MigrateIngresses {
return nil
}
log.Debug().Msgf("Migrating ingresses for environment %d", environment.ID)
err := migrator.kubeFactory.MigrateEndpointIngresses(&environment, migrator.dataStore, kubeclient)
if err != nil {
log.Error().Err(err).Msgf("Error migrating ingresses for environment %d", environment.ID)
return err
}
return nil
}
// MigrateGPUs will check all docker endpoints for containers with GPUs and set EnableGPUManagement to true if any are found
// If there's an error getting the containers, we'll log it and move on
func (migrator *PostInitMigrator) MigrateGPUs(e portainer.Endpoint, dockerClient *client.Client) error {
return migrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
environment, err := tx.Endpoint().Endpoint(e.ID)
if err != nil {
log.Error().Err(err).Msgf("Error getting environment %d", environment.ID)
return err
}
// Early exit if we do not need to migrate!
if !environment.PostInitMigrations.MigrateGPUs {
return nil
}
log.Debug().Msgf("Migrating GPUs for environment %d", e.ID)
// get all containers
containers, err := dockerClient.ContainerList(context.Background(), container.ListOptions{All: true})
if err != nil {
log.Error().Err(err).Msgf("failed to list containers for environment %d", environment.ID)
return err
}
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole environment
containersLoop:
for _, container := range containers {
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
if err != nil {
log.Error().Err(err).Msg("failed to inspect container")
continue
}
deviceRequests := containerDetails.HostConfig.Resources.DeviceRequests
for _, deviceRequest := range deviceRequests {
if deviceRequest.Driver == "nvidia" {
environment.EnableGPUManagement = true
break containersLoop
}
}
}
// set the MigrateGPUs flag to false so we don't run this again
environment.PostInitMigrations.MigrateGPUs = false
err = tx.Endpoint().UpdateEndpoint(environment.ID, environment)
if err != nil {
log.Error().Err(err).Msgf("Error updating EnableGPUManagement flag for environment %d", environment.ID)
return err
}
return nil
})
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
"github.com/portainer/portainer/api/dataservices/pendingactions"
"github.com/portainer/portainer/api/dataservices/registry"
@@ -43,6 +42,7 @@ import (
// Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system.
type Store struct {
flags *portainer.CLIFlags
connection portainer.Connection
fileService portainer.FileService
@@ -55,7 +55,6 @@ type Store struct {
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
HelmUserRepositoryService *helmuserrepository.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
@@ -101,7 +100,9 @@ func (store *Store) initServices() error {
}
store.EndpointRelationService = endpointRelationService
edgeStackService, err := edgestack.NewService(store.connection, endpointRelationService.InvalidateEdgeCacheForEdgeStack)
edgeStackService, err := edgestack.NewService(store.connection, func(tx portainer.Transaction, ID portainer.EdgeStackID) {
endpointRelationService.Tx(tx).InvalidateEdgeCacheForEdgeStack(ID)
})
if err != nil {
return err
}
@@ -138,12 +139,6 @@ func (store *Store) initServices() error {
}
store.ExtensionService = extensionService
fdoProfilesService, err := fdoprofile.NewService(store.connection)
if err != nil {
return err
}
store.FDOProfilesService = fdoProfilesService
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
if err != nil {
return err
@@ -289,11 +284,6 @@ func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
return store.EndpointRelationService
}
// FDOProfile gives access to the FDOProfile data management layer
func (store *Store) FDOProfile() dataservices.FDOProfileService {
return store.FDOProfilesService
}
// HelmUserRepository access the helm user repository settings
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
return store.HelmUserRepositoryService
@@ -398,11 +388,10 @@ type storeExport struct {
User []portainer.User `json:"users,omitempty"`
Version models.Version `json:"version,omitempty"`
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
func (store *Store) Export(filename string) (err error) {
backup := storeExport{}
if c, err := store.CustomTemplate().ReadAll(); err != nil {
@@ -606,6 +595,7 @@ func (store *Store) Export(filename string) (err error) {
if err != nil {
return err
}
return os.WriteFile(filename, b, 0600)
}
@@ -616,7 +606,7 @@ func (store *Store) Import(filename string) (err error) {
if err != nil {
return err
}
err = json.Unmarshal([]byte(s), &backup)
err = json.Unmarshal(s, &backup)
if err != nil {
return err
}

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