Compare commits

...

298 Commits

Author SHA1 Message Date
Malcolm Lockyer
3019e8ec11 chore: bump version to 2.31.1 (#815) 2025-06-19 11:37:00 +12:00
James Player
4b5b682d0c feat(k8s): CRD in applications list and details 2.31.1 - [R8S-357] (#806)
Co-authored-by: stevensbkang <skan070@gmail.com>
2025-06-18 09:06:58 +12:00
Yajith Dayarathna
078dca33b8 fix(api-documentation): swagger document genration error (#794) 2025-06-12 13:39:15 +12:00
Malcolm Lockyer
17ebe221bb chore: bump version to 2.31.0 (#789) 2025-06-10 16:47:17 +12:00
Ali
1963edda66 feat(helm): add registry dropdown [r8s-340] (#779) 2025-06-09 20:08:50 +12:00
Cara Ryan
c9e3717ce3 fix(kubernetes): Display more than 10 workloads under Helm expandable in the Applications view [R8S-339] (#781) 2025-06-09 15:12:24 +12:00
Oscar Zhou
9a85246631 fix(edgestack): display deploying status by default after creating edgestack [BE-11924] (#783) 2025-06-07 09:06:57 +12:00
andres-portainer
75f165d1ff feat(edgestackstatus): optimize the Edge Stack structures BE-11740 (#756) 2025-06-05 19:46:10 -03:00
Viktor Pettersson
eaf0deb2f6 feat(update-schedules): new update schedules view [BE-11754, BE-11887] (#686) 2025-06-05 17:03:43 +12:00
Ali
a9061e5258 feat(helm): enhance helm chart install [r8s-341] (#766) 2025-06-05 13:13:45 +12:00
James Player
caac45b834 feat(UI): Add repository url to Helm chart installation list items (#769) 2025-06-05 10:14:39 +12:00
LP B
24ff7a7911 chore(deps): upgrade docker/cli to v28.2.1 | docker/docker to v28.2.1 | docker/compose to v2.36.2 (#758) 2025-05-30 09:12:27 +02:00
Devon Steenberg
b767dcb27e fix(proxy): whitelist headers for proxy to forward [BE-11819] (#665) 2025-05-30 11:49:23 +12:00
Cara Ryan
731afbee46 feat(helm): filter on chart versions at API level [R8S-324] (#754) 2025-05-27 15:20:28 +12:00
Cara Ryan
07dfd981a2 fix(kubernetes): events api to call the backend [R8S-243] (#563) 2025-05-27 13:55:31 +12:00
Cara Ryan
32ef208278 Revert "feat(helm): filter on chart versions at API level [R8S-324]" (#753) 2025-05-26 16:58:53 +12:00
Cara Ryan
a80b185e10 feat(helm): filter on chart versions at API level [R8S-324] (#747) 2025-05-26 14:10:38 +12:00
Malcolm Lockyer
b96328e098 fix(async-perf): In async poll snapshot handling, reduce redundant json marshal [be-11861] (#726) 2025-05-23 12:42:45 +12:00
Devon Steenberg
45471ce86d fix(docker): check len of device capabilities [BE-11898] (#750) 2025-05-22 14:27:14 +12:00
Viktor Pettersson
1bc91d0c7c fix(edge-update): set edge stack status to EdgeStackStatusError to avoid redeployment of portainer-updater [BE-11855] (#714) 2025-05-20 08:28:40 +02:00
James Carppe
799325d9f8 Update bug report template for 2.30.1 (#749) 2025-05-20 14:40:43 +12:00
James Carppe
b540709e03 Update bug report template for 2.30.0 (#737) 2025-05-15 12:09:28 +12:00
Oscar Zhou
44daab04ac fix(libclient): option to disable external http request [BE-11696] (#719) 2025-05-15 09:54:35 +12:00
Ali
ee65223ee7 chore: bump version to 2.30.0 (#735) 2025-05-14 17:35:05 +12:00
Ali
d49fcd8f3e feat(helm): make the atomic flag optional [r8s-314] (#733) 2025-05-14 16:31:42 +12:00
Ali
4ee349bd6b feat(helm): helm actions [r8s-259] (#715)
Co-authored-by: James Player <james.player@portainer.io>
Co-authored-by: Cara Ryan <cara.ryan@portainer.io>
Co-authored-by: stevensbkang <skan070@gmail.com>
2025-05-13 22:15:04 +12:00
Steven Kang
dfa32b6755 chore: add KaaS deprecation notice (#727)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
2025-05-13 16:33:14 +12:00
Ali
0b69729173 chrore(microk8s): add deprecation notice [r8s-320] (#728) 2025-05-13 14:28:42 +12:00
Steven Kang
3b313b9308 fix(kubectl): rollout restart [r8s-322] (#729) 2025-05-13 11:35:44 +12:00
Devon Steenberg
1abdf42f99 feat(libstack): expose env vars with PORTAINER_ prefix [BE-11661] (#687) 2025-05-12 11:18:04 +12:00
andres-portainer
9fdc535d6b fix(csrf): skip the trusted origins check for plain-text HTTP requests BE-11832 (#710)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
2025-05-09 14:39:29 +12:00
James Carppe
b9b734ceda Update bug report template for 2.27.6 (#721) 2025-05-09 14:39:15 +12:00
Viktor Pettersson
3b05505527 fix(update-schedules): display enriched error logs for agent updates [BE-11756] (#693) 2025-05-08 10:24:20 +02:00
Steven Kang
bc29419c17 refactor: replace the kubectl binary with the upstream sdk (#524) 2025-05-07 20:40:38 +12:00
James Carppe
4d4360b86b Update bug report template for 2.27.5 (#705) 2025-05-02 13:14:39 +12:00
James Carppe
8cc28761d7 Update bug report template for 2.29.2 (#692) 2025-04-24 16:47:31 +12:00
Viktor Pettersson
24b3499c70 fix(dependencies): downgrade gorilla/csrf to v1.7.2 (#684) 2025-04-24 12:13:45 +12:00
Devon Steenberg
4e4fd5a4b4 fix(validate): refactor validate functions [BE-11574] (#683) 2025-04-24 08:59:44 +12:00
Devon Steenberg
1a3df54c04 fix(govalidator): replace govalidator dependency [BE-11574] (#673) 2025-04-23 13:59:51 +12:00
James Carppe
3edacee59b Update bug report template for 2.29.1 (#682) 2025-04-23 13:35:20 +12:00
andres-portainer
f25d31b92b fix(code): remove dead code and reduce duplication BE-11826 (#680) 2025-04-22 18:09:36 -03:00
Ali
c91c8a6467 feat(helm): rollback helm chart [r8s-287] (#660) 2025-04-23 08:58:34 +12:00
Ali
61d6ac035d feat(helm): auto refresh helm resources [r8s-298] (#672) 2025-04-23 08:58:21 +12:00
Oscar Zhou
9a9373dd0f fix: cve-2025-22871 [BE-11825] (#678) 2025-04-22 21:29:39 +12:00
andres-portainer
e319a7a5ae fix(linter): enable ineffassign BE-10204 (#669) 2025-04-21 19:27:14 -03:00
andres-portainer
342549b546 fix(validate): remove dead code BE-11824 (#671) 2025-04-21 18:59:51 -03:00
Ali
bbe94f55b6 feat(helm): uninstall helm app from details view [r8s-285] (#648) 2025-04-22 09:52:52 +12:00
andres-portainer
6fcf1893d3 fix(code): remove duplicated code BE-11821 (#667) 2025-04-18 17:34:34 -03:00
Ali
01afe34df7 fix(namespaces): fix service not found error [r8s-296] (#664) 2025-04-17 12:29:37 +12:00
Devon Steenberg
be3e8e3332 fix(proxy): don't forward sensitive headers [BE-11819] (#654) 2025-04-16 15:30:56 +12:00
James Carppe
cf31700903 Update bug report template for 2.29.0 (#655) 2025-04-16 13:34:38 +12:00
andres-portainer
66dee6fd06 fix(codemirror): optimize the autocompletion performance R8S-294 (#650)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2025-04-16 12:27:30 +12:00
andres-portainer
bfa55f8c67 fix(logs): remove duplicated code BE-11821 (#653) 2025-04-15 17:16:04 -03:00
James Carppe
5a2318d01f Update bug report template for 2.27.4 (#646) 2025-04-15 13:50:14 +12:00
Steven Kang
7de037029f security: cve-2025-30204 and other low ones - develop [BE-11781] (#638) 2025-04-15 09:58:55 +12:00
andres-portainer
730c1115ce fix(proxy): remove code duplication BE-11627 (#644) 2025-04-14 17:46:40 -03:00
Oscar Zhou
2c37f32fa6 version: bump version to 2.29.0 (#637) 2025-04-14 13:13:38 +12:00
LP B
7aa9f8b1c3 Revert "feat(app): 1s staleTime to avoid sending repeated requests" (#639) 2025-04-14 11:12:11 +12:00
LP B
c331ada086 feat(app): 1s staleTime to avoid sending repeated requests (#607) 2025-04-14 09:05:48 +12:00
Oscar Zhou
ebc25e45d3 fix(edge): redeploy edge stack doesn't apply to std agents [BE-11766] (#633) 2025-04-12 10:24:23 +12:00
andres-portainer
f82921d2a1 fix(edgestacks): fix edge stack update when using Git BE-11766 (#629) 2025-04-10 20:12:27 -03:00
Ali
d68fe42918 fix(apps): better align sub tables [r8s-255] (#617) 2025-04-11 08:39:39 +12:00
Oscar Zhou
823f2a7991 fix(edge): missing env var in async agent docker snapshot [BE-11709] (#625) 2025-04-11 08:26:11 +12:00
Ali
0ca9321db1 feat(helm): update helm view [r8s-256] (#582)
Co-authored-by: Cara Ryan <cara.ryan@portainer.io>
Co-authored-by: James Player <james.player@portainer.io>
Co-authored-by: stevensbkang <skan070@gmail.com>
2025-04-10 16:08:24 +12:00
James Player
46eddbe7b9 fix(UI): Make sure localStorage.getUserId actually returns user id R8S-290 (#623) 2025-04-09 09:09:07 +12:00
James Player
64c796a8c3 fix(kubernetes): Config maps and secrets show as unused BE-11684 (#596)
Co-authored-by: stevensbkang <skan070@gmail.com>
2025-04-08 12:52:21 +12:00
James Player
264ff5457b chore(kubernetes): Migrate Helm Templates View to React R8S-239 (#587) 2025-04-08 12:51:36 +12:00
LP B
ad89df4d0d refactor(app): reword docker security features (#608) 2025-04-07 17:14:51 +02:00
Anthony Lapenna
0f10b8ba2b api: update TeamInspect doc (#618) 2025-04-07 11:25:23 +12:00
Oscar Zhou
940bf990f9 fix(edgeconfig): add edge config file interpolation info message on edge stack page [BE-11741] (#606) 2025-04-04 11:56:42 +13:00
Devon Steenberg
1b8fbbe7d7 fix(libstack): compose project working directory [BE-11751] (#600) 2025-04-04 09:07:35 +13:00
James Player
f6f07f4690 improvement(kubernetes): right align tags in datatables R8S-250 (#601)
Co-authored-by: testA113 <aliharriss1995@gmail.com>
2025-04-03 14:18:31 +13:00
Anthony Lapenna
3800249921 api: use response code 200 (#604) 2025-04-03 11:12:24 +13:00
Oscar Zhou
a5d857d5e7 feat(docker): add --pull-limit-check-disabled cli flag [BE-11739] (#581) 2025-04-03 09:13:01 +13:00
Devon Steenberg
4c1e80ff58 fix(axios): correctly encode urls [BE-11648] (#517)
fix(edgegroup): nil pointer defer
2025-04-02 08:51:58 +13:00
Oscar Zhou
7e5db1f55e refactor(edgegroup): optimize edge group search performance [BE-11716] (#579) 2025-04-01 14:05:56 +13:00
Anthony Lapenna
1edc56c0ce api: remove name from edgegroupupdate payload validation (#588) 2025-04-01 13:25:09 +13:00
Anthony Lapenna
4066a70ea5 api: fix typo in operation name (#585) 2025-04-01 13:24:55 +13:00
andres-portainer
a0d36cf87a fix(server): add panic logging middleware BE-11750 (#599) 2025-03-31 18:58:20 -03:00
Viktor Pettersson
1d12011eb5 fix(edge groups): make large edge groups editable [BE-11720] (#558) 2025-03-28 15:16:05 +01:00
Steven Kang
7c01f84a5c fix: improve the node view for detecting roles - develop (#354) 2025-03-28 10:52:59 +13:00
Ali
81c5f4acc3 feat(editor): provide yaml validation for docker compose in the portainer web editor [BE-11697] (#526) 2025-03-27 17:11:55 +13:00
Ali
0ebfe047d1 feat(helm): use helm upgrade for install [r8s-258] (#568) 2025-03-26 11:32:26 +13:00
samdulam
e68bd53e30 Update bug_report template with 2.27.3 (#572) 2025-03-25 08:40:15 +05:30
andres-portainer
cdd9851f72 fix(stubs): clean up the stubs and mocks BE-11722 (#557) 2025-03-24 19:56:08 -03:00
andres-portainer
995c3ef81b feat(snapshots): avoid parsing raw snapshots when possible BE-11724 (#560) 2025-03-24 19:33:05 -03:00
James Player
0dfde1374d fix(kubernetes): Cluster reservation CPU not showing R8S-268 (#569) 2025-03-25 10:59:28 +13:00
Devon Steenberg
34235199dd fix(libstack): correctly load COMPOSE_* env vars [BE-11474] (#536) 2025-03-25 08:57:23 +13:00
Anthony Lapenna
5d1cd670e9 docs: review TeamMembershipCreate API operation (#565) 2025-03-24 09:55:33 +13:00
Anthony Lapenna
1d8ea7b0ee docs: review TeamUpdate API operation (#564) 2025-03-21 16:45:43 +13:00
Oscar Zhou
4b218553c3 fix(libstack): data loss for stack with relative path [FR-437] (#548) 2025-03-21 09:19:25 +13:00
Viktor Pettersson
a61c1004d3 fix(agent-updates): fix remote agent updates cannot be scheduled properly for large edge groups [BE-11691] (#528) 2025-03-20 10:05:15 +01:00
James Carppe
5d1b42b314 Update bug report template for 2.28.1 (#549) 2025-03-20 15:54:53 +13:00
Oscar Zhou
4b992c6f3e fix(k8s/config): force insecure-skip-tls-verify option for internal use [BE-11706] (#537) 2025-03-20 08:49:27 +13:00
Viktor Pettersson
38562f9560 fix(api): remove duplicated /users/me route [BE-11689] (#516) 2025-03-19 13:08:03 +01:00
James Carppe
c01f0271fe Update bug report template for 2.27.2 (#539) 2025-03-19 17:41:36 +13:00
andres-portainer
0296998fae fix(users): optimize the /users/me API endpoint BE-11688 (#515)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
Co-authored-by: JamesPlayer <james.player@portainer.io>
2025-03-18 17:55:53 -03:00
James Player
a67b917bdd Bump version to 2.28.0 (#523) 2025-03-17 16:00:33 +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
1092 changed files with 37261 additions and 20600 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

@@ -2,7 +2,6 @@ name: Bug Report
description: Create a report to help us improve. description: Create a report to help us improve.
labels: kind/bug,bug/need-confirmation labels: kind/bug,bug/need-confirmation
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
@@ -12,6 +11,8 @@ body:
You can also ask for help in our [community Slack channel](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA). 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**. **DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS**.
- type: checkboxes - type: checkboxes
@@ -90,33 +91,37 @@ body:
- type: dropdown - type: dropdown
attributes: attributes:
label: Portainer version 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 [updating first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
multiple: false multiple: false
options: options:
- '2.30.1'
- '2.30.0'
- '2.29.2'
- '2.29.1'
- '2.29.0'
- '2.28.1'
- '2.28.0'
- '2.27.6'
- '2.27.5'
- '2.27.4'
- '2.27.3'
- '2.27.2'
- '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.22.0'
- '2.21.5'
- '2.21.4'
- '2.21.3' - '2.21.3'
- '2.21.2' - '2.21.2'
- '2.21.1' - '2.21.1'
- '2.21.0' - '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: validations:
required: true required: true

View File

@@ -1,166 +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
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: 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-file: go.mod
- 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 --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} --build-arg OSVERSION=${{ matrix.config.version }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
else
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" -f build/${{ matrix.config.platform }}/Dockerfile .
docker buildx build --output=type=registry --attest type=provenance,mode=max --attest type=sbom,disabled=false --platform ${{ matrix.config.platform }}/${{ matrix.config.arch }} -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-alpine" -f build/${{ matrix.config.platform }}/alpine.Dockerfile .
fi
fi
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}-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" \
"${DOCKER_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-ppc64le-alpine"
if [[ "${GITHUB_REF_NAME}" =~ ^release/.*$ ]]; then
docker buildx imagetools create -t "${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-amd64" \
"${EXTENSION_HUB_REPO}:${CONTAINER_IMAGE_TAG}-linux-arm64"
fi

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.22.5
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.59.1
args: --timeout=10m -c .golangci.yaml

View File

@@ -1,254 +0,0 @@
name: Nightly Code Security Scan
on:
schedule:
- cron: '0 20 * * *'
workflow_dispatch:
env:
GO_VERSION: 1.22.5
DOCKER_HUB_REPO: portainerci/portainer-ce
DOCKER_HUB_IMAGE_TAG: develop
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 ${{ env.DOCKER_HUB_REPO }}:${{ env.DOCKER_HUB_IMAGE_TAG }}
- 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: ${{ env.DOCKER_HUB_REPO }}:${{ env.DOCKER_HUB_IMAGE_TAG }}
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.22.5
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,76 +0,0 @@
name: Test
env:
GO_VERSION: 1.22.5
NODE_VERSION: 18.x
on:
workflow_dispatch:
pull_request:
branches:
- master
- develop
- release/*
types:
- opened
- reopened
- synchronize
- ready_for_review
push:
branches:
- master
- develop
- release/*
jobs:
test-client:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up node.js'
uses: actions/setup-node@v4.0.1
with:
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:
- name: 'checkout the current branch'
uses: actions/checkout@v4.1.1
with:
ref: ${{ github.event.inputs.branch }}
- name: 'set up golang'
uses: actions/setup-go@v5.0.0
with:
go-version: ${{ env.GO_VERSION }}
- name: 'install dependencies'
run: make test-deps PLATFORM=linux ARCH=amd64
- name: 'update $PATH'
run: echo "$(pwd)/dist" >> $GITHUB_PATH
- name: 'run tests'
run: make test-server

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.22.5
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,6 +9,10 @@ linters:
- gosimple - gosimple
- govet - govet
- errorlint - errorlint
- copyloopvar
- intrange
- perfsprint
- ineffassign
linters-settings: linters-settings:
depguard: depguard:
@@ -17,8 +21,6 @@ linters-settings:
deny: deny:
- pkg: 'encoding/json' - pkg: 'encoding/json'
desc: 'use github.com/segmentio/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' - pkg: 'golang.org/x/exp'
desc: 'exp is not allowed' desc: 'exp is not allowed'
- pkg: 'github.com/portainer/libcrypto' - pkg: 'github.com/portainer/libcrypto'

View File

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

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

@@ -17,11 +17,13 @@ GOTESTSUM=go run gotest.tools/gotestsum@latest
##@ Building ##@ 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: init-dist:
@mkdir -p 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 build-client: init-dist ## Build the client
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG) export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
@@ -50,7 +52,7 @@ client-deps: ## Install client dependencies
yarn yarn
tidy: ## Tidy up the go.mod file tidy: ## Tidy up the go.mod file
cd api && go mod tidy @go mod tidy
##@ Cleanup ##@ Cleanup
@@ -64,14 +66,11 @@ clean: ## Remove all build and download artifacts
.PHONY: test test-client test-server .PHONY: test test-client test-server
test: test-server test-client ## Run all tests test: test-server test-client ## Run all tests
test-deps: init-dist
./build/download_docker_compose_binary.sh $(PLATFORM) $(ARCH) $(shell jq -r '.dockerCompose' < "./binary-version.json")
test-client: ## Run client tests test-client: ## Run client tests
yarn test $(ARGS) yarn test $(ARGS) --coverage
test-server: ## Run server tests 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 ##@ Dev
.PHONY: dev dev-client dev-server .PHONY: dev dev-client dev-server

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ package archive
import ( import (
"archive/zip" "archive/zip"
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -12,50 +11,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// UnzipArchive will unzip an archive from bytes into the dest destination folder on disk
func UnzipArchive(archiveData []byte, dest string) error {
zipReader, err := zip.NewReader(bytes.NewReader(archiveData), int64(len(archiveData)))
if err != nil {
return err
}
for _, zipFile := range zipReader.File {
err := extractFileFromArchive(zipFile, dest)
if err != nil {
return err
}
}
return nil
}
func extractFileFromArchive(file *zip.File, dest string) error {
f, err := file.Open()
if err != nil {
return err
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return err
}
fpath := filepath.Join(dest, file.Name)
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
_, err = io.Copy(outFile, bytes.NewReader(data))
if err != nil {
return err
}
return outFile.Close()
}
// UnzipFile will decompress a zip archive, moving all files and folders // UnzipFile will decompress a zip archive, moving all files and folders
// within the zip file (parameter 1) to an output directory (parameter 2). // within the zip file (parameter 1) to an output directory (parameter 2).
func UnzipFile(src string, dest string) error { func UnzipFile(src string, dest string) error {
@@ -76,11 +31,11 @@ func UnzipFile(src string, dest string) error {
if f.FileInfo().IsDir() { if f.FileInfo().IsDir() {
// Make Folder // Make Folder
os.MkdirAll(p, os.ModePerm) os.MkdirAll(p, os.ModePerm)
continue continue
} }
err = unzipFile(f, p) if err := unzipFile(f, p); err != nil {
if err != nil {
return err return err
} }
} }
@@ -93,20 +48,20 @@ func unzipFile(f *zip.File, p string) error {
if err := os.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil { if err := os.MkdirAll(filepath.Dir(p), os.ModePerm); err != nil {
return errors.Wrapf(err, "unzipFile: can't make a path %s", p) return errors.Wrapf(err, "unzipFile: can't make a path %s", p)
} }
outFile, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) outFile, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil { if err != nil {
return errors.Wrapf(err, "unzipFile: can't create file %s", p) return errors.Wrapf(err, "unzipFile: can't create file %s", p)
} }
defer outFile.Close() defer outFile.Close()
rc, err := f.Open() rc, err := f.Open()
if err != nil { if err != nil {
return errors.Wrapf(err, "unzipFile: can't open zip file %s in the archive", f.Name) return errors.Wrapf(err, "unzipFile: can't open zip file %s in the archive", f.Name)
} }
defer rc.Close() defer rc.Close()
_, err = io.Copy(outFile, rc) if _, err = io.Copy(outFile, rc); err != nil {
if err != nil {
return errors.Wrapf(err, "unzipFile: can't copy an archived file content") return errors.Wrapf(err, "unzipFile: can't copy an archived file content")
} }

View File

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

View File

@@ -21,6 +21,7 @@ const rwxr__r__ os.FileMode = 0o744
var filesToBackup = []string{ var filesToBackup = []string{
"certs", "certs",
"chisel",
"compose", "compose",
"config.json", "config.json",
"custom_templates", "custom_templates",
@@ -30,40 +31,13 @@ var filesToBackup = []string{
"portainer.key", "portainer.key",
"portainer.pub", "portainer.pub",
"tls", "tls",
"chisel",
} }
// Creates a tar.gz system archive and encrypts it if password is not empty. Returns a path to the archive file. // 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) { func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datastore dataservices.DataStore, filestorePath string) (string, error) {
unlock := gate.Lock() backupDirPath, err := backupDatabaseAndFilesystem(gate, datastore, filestorePath)
defer unlock() if err != nil {
return "", err
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")
}
} }
archivePath, err := archive.TarGzDir(backupDirPath) archivePath, err := archive.TarGzDir(backupDirPath)
@@ -81,6 +55,37 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
return archivePath, nil 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 { func backupDb(backupDirPath string, datastore dataservices.DataStore) error {
dbFileName := datastore.Connection().GetDatabaseFileName() dbFileName := datastore.Connection().GetDatabaseFileName()
_, err := datastore.Backup(filepath.Join(backupDirPath, dbFileName)) _, err := datastore.Backup(filepath.Join(backupDirPath, dbFileName))
@@ -94,7 +99,7 @@ func encrypt(path string, passphrase string) (string, error) {
} }
defer in.Close() defer in.Close()
outFileName := fmt.Sprintf("%s.encrypted", path) outFileName := path + ".encrypted"
out, err := os.Create(outFileName) out, err := os.Create(outFileName)
if err != nil { if err != nil {
return "", err 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,82 +0,0 @@
package chisel
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/edge/cache"
)
// EdgeJobs retrieves the edge jobs for the given environment
func (service *Service) EdgeJobs(endpointID portainer.EndpointID) []portainer.EdgeJob {
service.mu.RLock()
defer service.mu.RUnlock()
return append(
make([]portainer.EdgeJob, 0, len(service.edgeJobs[endpointID])),
service.edgeJobs[endpointID]...,
)
}
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
func (service *Service) AddEdgeJob(endpoint *portainer.Endpoint, edgeJob *portainer.EdgeJob) {
if endpoint.Edge.AsyncMode {
return
}
service.mu.Lock()
defer service.mu.Unlock()
existingJobIndex := -1
for idx, existingJob := range service.edgeJobs[endpoint.ID] {
if existingJob.ID == edgeJob.ID {
existingJobIndex = idx
break
}
}
if existingJobIndex == -1 {
service.edgeJobs[endpoint.ID] = append(service.edgeJobs[endpoint.ID], *edgeJob)
} else {
service.edgeJobs[endpoint.ID][existingJobIndex] = *edgeJob
}
cache.Del(endpoint.ID)
}
// 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 := range service.edgeJobs {
n := 0
for _, edgeJob := range service.edgeJobs[endpointID] {
if edgeJob.ID != edgeJobID {
service.edgeJobs[endpointID][n] = edgeJob
n++
}
}
service.edgeJobs[endpointID] = service.edgeJobs[endpointID][:n]
cache.Del(endpointID)
}
service.mu.Unlock()
}
func (service *Service) RemoveEdgeJobFromEndpoint(endpointID portainer.EndpointID, edgeJobID portainer.EdgeJobID) {
service.mu.Lock()
defer service.mu.Unlock()
n := 0
for _, edgeJob := range service.edgeJobs[endpointID] {
if edgeJob.ID != edgeJobID {
service.edgeJobs[endpointID][n] = edgeJob
n++
}
}
service.edgeJobs[endpointID] = service.edgeJobs[endpointID][:n]
cache.Del(endpointID)
}

View File

@@ -59,6 +59,8 @@ func CLIFlags() *portainer.CLIFlags {
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(), SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"), LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("NOCOLOR", "PRETTY", "JSON"), 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(),
PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(),
} }
} }

View File

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

View File

@@ -4,20 +4,21 @@
package cli package cli
const ( const (
defaultBindAddress = ":9000" defaultBindAddress = ":9000"
defaultHTTPSBindAddress = ":9443" defaultHTTPSBindAddress = ":9443"
defaultTunnelServerAddress = "0.0.0.0" defaultTunnelServerAddress = "0.0.0.0"
defaultTunnelServerPort = "8000" defaultTunnelServerPort = "8000"
defaultDataDirectory = "/data" defaultDataDirectory = "/data"
defaultAssetsDirectory = "./" defaultAssetsDirectory = "./"
defaultTLS = "false" defaultTLS = "false"
defaultTLSSkipVerify = "false" defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "/certs/ca.pem" defaultTLSCACertPath = "/certs/ca.pem"
defaultTLSCertPath = "/certs/cert.pem" defaultTLSCertPath = "/certs/cert.pem"
defaultTLSKeyPath = "/certs/key.pem" defaultTLSKeyPath = "/certs/key.pem"
defaultHTTPDisabled = "false" defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false" defaultHTTPEnabled = "false"
defaultSSL = "false" defaultSSL = "false"
defaultBaseURL = "/" defaultBaseURL = "/"
defaultSecretKeyName = "portainer" defaultSecretKeyName = "portainer"
defaultPullLimitCheckDisabled = "false"
) )

View File

@@ -1,21 +1,22 @@
package cli package cli
const ( const (
defaultBindAddress = ":9000" defaultBindAddress = ":9000"
defaultHTTPSBindAddress = ":9443" defaultHTTPSBindAddress = ":9443"
defaultTunnelServerAddress = "0.0.0.0" defaultTunnelServerAddress = "0.0.0.0"
defaultTunnelServerPort = "8000" defaultTunnelServerPort = "8000"
defaultDataDirectory = "C:\\data" defaultDataDirectory = "C:\\data"
defaultAssetsDirectory = "./" defaultAssetsDirectory = "./"
defaultTLS = "false" defaultTLS = "false"
defaultTLSSkipVerify = "false" defaultTLSSkipVerify = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem" defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultTLSCertPath = "C:\\certs\\cert.pem" defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultTLSKeyPath = "C:\\certs\\key.pem" defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultHTTPDisabled = "false" defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false" defaultHTTPEnabled = "false"
defaultSSL = "false" defaultSSL = "false"
defaultSnapshotInterval = "5m" defaultSnapshotInterval = "5m"
defaultBaseURL = "/" defaultBaseURL = "/"
defaultSecretKeyName = "portainer" defaultSecretKeyName = "portainer"
defaultPullLimitCheckDisabled = "false"
) )

View File

@@ -1,45 +0,0 @@
package cli
import (
"strings"
portainer "github.com/portainer/portainer/api"
"gopkg.in/alecthomas/kingpin.v2"
)
type pairListBool []portainer.Pair
// Set implementation for a list of portainer.Pair
func (l *pairListBool) Set(value string) error {
p := new(portainer.Pair)
// default to true. example setting=true is equivalent to setting
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
p.Name = parts[0]
p.Value = "true"
} else {
p.Name = parts[0]
p.Value = parts[1]
}
*l = append(*l, *p)
return nil
}
// String implementation for a list of pair
func (l *pairListBool) String() string {
return ""
}
// IsCumulative implementation for a list of pair
func (l *pairListBool) IsCumulative() bool {
return true
}
func BoolPairs(s kingpin.Settings) (target *[]portainer.Pair) {
target = new([]portainer.Pair)
s.SetValue((*pairListBool)(target))
return
}

View File

@@ -10,7 +10,6 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey" "github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/build"
"github.com/portainer/portainer/api/chisel" "github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli" "github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/crypto"
@@ -31,7 +30,6 @@ import (
"github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/edge/edgestacks" "github.com/portainer/portainer/api/internal/edge/edgestacks"
"github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/snapshot" "github.com/portainer/portainer/api/internal/snapshot"
@@ -41,6 +39,7 @@ import (
"github.com/portainer/portainer/api/kubernetes" "github.com/portainer/portainer/api/kubernetes"
kubecli "github.com/portainer/portainer/api/kubernetes/cli" kubecli "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/ldap" "github.com/portainer/portainer/api/ldap"
"github.com/portainer/portainer/api/logs"
"github.com/portainer/portainer/api/oauth" "github.com/portainer/portainer/api/oauth"
"github.com/portainer/portainer/api/pendingactions" "github.com/portainer/portainer/api/pendingactions"
"github.com/portainer/portainer/api/pendingactions/actions" "github.com/portainer/portainer/api/pendingactions/actions"
@@ -48,9 +47,10 @@ import (
"github.com/portainer/portainer/api/platform" "github.com/portainer/portainer/api/platform"
"github.com/portainer/portainer/api/scheduler" "github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks/deployments" "github.com/portainer/portainer/api/stacks/deployments"
"github.com/portainer/portainer/pkg/build"
"github.com/portainer/portainer/pkg/featureflags" "github.com/portainer/portainer/pkg/featureflags"
"github.com/portainer/portainer/pkg/libhelm" "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/portainer/portainer/pkg/libstack/compose"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
@@ -95,7 +95,7 @@ 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") 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() isNew, err := store.Open()
if err != nil { if err != nil {
@@ -122,7 +122,7 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
log.Fatal().Err(err).Msg("failed generating instance id") log.Fatal().Err(err).Msg("failed generating instance id")
} }
migratorInstance := migrator.NewMigrator(&migrator.MigratorParameters{}) migratorInstance := migrator.NewMigrator(&migrator.MigratorParameters{Flags: flags})
migratorCount := migratorInstance.GetMigratorCountOfCurrentAPIVersion() migratorCount := migratorInstance.GetMigratorCountOfCurrentAPIVersion()
// from MigrateData // from MigrateData
@@ -167,32 +167,12 @@ func checkDBSchemaServerVersionMatch(dbStore dataservices.DataStore, serverVersi
return v.SchemaVersion == serverVersion && v.Edition == serverEdition return v.SchemaVersion == serverVersion && v.Edition == serverEdition
} }
func initComposeStackManager(composeDeployer libstack.Deployer, proxyManager *proxy.Manager) portainer.ComposeStackManager { func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager) portainer.KubernetesDeployer {
composeWrapper, err := exec.NewComposeStackManager(composeDeployer, proxyManager) return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager)
if err != nil {
log.Fatal().Err(err).Msg("failed creating compose manager")
}
return composeWrapper
} }
func initSwarmStackManager( func initHelmPackageManager() (libhelmtypes.HelmPackageManager, error) {
assetsPath string, return libhelm.NewHelmPackageManager()
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 initAPIKeyService(datastore dataservices.DataStore) apikey.APIKeyService { func initAPIKeyService(datastore dataservices.DataStore) apikey.APIKeyService {
@@ -260,10 +240,10 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
return err return err
} }
settings.SnapshotInterval = *cmp.Or(flags.SnapshotInterval, &settings.SnapshotInterval) settings.SnapshotInterval = cmp.Or(*flags.SnapshotInterval, settings.SnapshotInterval)
settings.LogoURL = *cmp.Or(flags.Logo, &settings.LogoURL) settings.LogoURL = cmp.Or(*flags.Logo, settings.LogoURL)
settings.EnableEdgeComputeFeatures = *cmp.Or(flags.EnableEdgeComputeFeatures, &settings.EnableEdgeComputeFeatures) settings.EnableEdgeComputeFeatures = cmp.Or(*flags.EnableEdgeComputeFeatures, settings.EnableEdgeComputeFeatures)
settings.TemplatesURL = *cmp.Or(flags.Templates, &settings.TemplatesURL) settings.TemplatesURL = cmp.Or(*flags.Templates, settings.TemplatesURL)
if *flags.Labels != nil { if *flags.Labels != nil {
settings.BlackListedLabels = *flags.Labels settings.BlackListedLabels = *flags.Labels
@@ -434,19 +414,16 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
dockerConfigPath := fileService.GetDockerConfigPath() dockerConfigPath := fileService.GetDockerConfigPath()
composeDeployer, err := compose.NewComposeDeployer(*flags.Assets, dockerConfigPath) composeDeployer := compose.NewComposeDeployer()
if err != nil {
log.Fatal().Err(err).Msg("failed initializing compose deployer")
}
composeStackManager := initComposeStackManager(composeDeployer, proxyManager) composeStackManager := exec.NewComposeStackManager(composeDeployer, proxyManager, dataStore)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore) swarmStackManager, err := exec.NewSwarmStackManager(*flags.Assets, dockerConfigPath, signatureService, fileService, reverseTunnelService, dataStore)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed initializing swarm stack manager") log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
} }
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, *flags.Assets) kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager)
pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory) pendingActionsService := pendingactions.NewService(dataStore, kubernetesClientFactory)
pendingActionsService.RegisterHandler(actions.CleanNAPWithOverridePolicies, handlers.NewHandlerCleanNAPWithOverridePolicies(authorizationService, dataStore)) pendingActionsService.RegisterHandler(actions.CleanNAPWithOverridePolicies, handlers.NewHandlerCleanNAPWithOverridePolicies(authorizationService, dataStore))
@@ -462,15 +439,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService) proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService)
helmPackageManager, err := initHelmPackageManager(*flags.Assets) helmPackageManager, err := initHelmPackageManager()
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed initializing helm package manager") log.Fatal().Err(err).Msg("failed initializing helm package manager")
} }
if err := edge.LoadEdgeJobs(dataStore, reverseTunnelService); err != nil {
log.Fatal().Err(err).Msg("failed loading edge jobs from database")
}
applicationStatus := initStatus(instanceID) applicationStatus := initStatus(instanceID)
// channel to control when the admin user is created // channel to control when the admin user is created
@@ -604,17 +577,18 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
AdminCreationDone: adminCreationDone, AdminCreationDone: adminCreationDone,
PendingActionsService: pendingActionsService, PendingActionsService: pendingActionsService,
PlatformService: platformService, PlatformService: platformService,
PullLimitCheckDisabled: *flags.PullLimitCheckDisabled,
} }
} }
func main() { func main() {
configureLogger() logs.ConfigureLogger()
setLoggingMode("PRETTY") logs.SetLoggingMode("PRETTY")
flags := initCLI() flags := initCLI()
setLoggingLevel(*flags.LogLevel) logs.SetLoggingLevel(*flags.LogLevel)
setLoggingMode(*flags.LogMode) logs.SetLoggingMode(*flags.LogMode)
for { for {
server := buildServer(flags) server := buildServer(flags)

View File

@@ -6,8 +6,10 @@ import (
type ReadTransaction interface { type ReadTransaction interface {
GetObject(bucketName string, key []byte, object any) error GetObject(bucketName string, key []byte, object any) error
GetRawBytes(bucketName string, key []byte) ([]byte, error)
GetAll(bucketName string, obj any, append func(o any) (any, error)) 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 GetAllWithKeyPrefix(bucketName string, keyPrefix []byte, obj any, append func(o any) (any, error)) error
KeyExists(bucketName string, key []byte) (bool, error)
} }
type Transaction interface { type Transaction interface {
@@ -40,6 +42,7 @@ type Connection interface {
GetDatabaseFileName() string GetDatabaseFileName() string
GetDatabaseFilePath() string GetDatabaseFilePath() string
GetStorePath() string GetStorePath() string
GetDatabaseFileSize() (int64, error)
IsEncryptedStore() bool IsEncryptedStore() bool
NeedsEncryptionMigration() (bool, error) NeedsEncryptionMigration() (bool, error)

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 // 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 { func AesEncrypt(input io.Reader, output io.Writer, passphrase []byte) error {
err := aesEncryptGCM(input, output, passphrase) if err := aesEncryptGCM(input, output, passphrase); err != nil {
if err != nil {
return fmt.Errorf("error encrypting file: %w", err) 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 { if string(header) != aesGcmHeader {
return nil, fmt.Errorf("invalid header") return nil, errors.New("invalid header")
} }
// Read salt // Read salt
@@ -194,8 +193,7 @@ func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
return nil, err return nil, err
} }
_, err = buf.Write(plaintext) if _, err := buf.Write(plaintext); err != nil {
if err != nil {
return nil, err return nil, err
} }

View File

@@ -62,6 +62,15 @@ func (connection *DbConnection) GetStorePath() string {
return connection.Path 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) { func (connection *DbConnection) SetEncrypted(flag bool) {
connection.isEncrypted = flag connection.isEncrypted = flag
} }
@@ -235,6 +244,32 @@ func (connection *DbConnection) GetObject(bucketName string, key []byte, object
}) })
} }
func (connection *DbConnection) GetRawBytes(bucketName string, key []byte) ([]byte, error) {
var value []byte
err := connection.ViewTx(func(tx portainer.Transaction) error {
var err error
value, err = tx.GetRawBytes(bucketName, key)
return err
})
return value, err
}
func (connection *DbConnection) KeyExists(bucketName string, key []byte) (bool, error) {
var exists bool
err := connection.ViewTx(func(tx portainer.Transaction) error {
var err error
exists, err = tx.KeyExists(bucketName, key)
return err
})
return exists, err
}
func (connection *DbConnection) getEncryptionKey() []byte { func (connection *DbConnection) getEncryptionKey() []byte {
if !connection.isEncrypted { if !connection.isEncrypted {
return nil return nil

View File

@@ -49,8 +49,8 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
backup["__metadata"] = meta backup["__metadata"] = meta
} }
err = connection.View(func(tx *bolt.Tx) error { if err := connection.View(func(tx *bolt.Tx) error {
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error { return tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name) bucketName := string(name)
var list []any var list []any
version := make(map[string]string) version := make(map[string]string)
@@ -84,27 +84,22 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
return nil return nil
} }
if len(list) > 0 { if bucketName == "ssl" ||
if bucketName == "ssl" || bucketName == "settings" ||
bucketName == "settings" || bucketName == "tunnel_server" {
bucketName == "tunnel_server" { backup[bucketName] = nil
backup[bucketName] = nil if len(list) > 0 {
if len(list) > 0 { backup[bucketName] = list[0]
backup[bucketName] = list[0]
}
return nil
} }
backup[bucketName] = list
return nil return nil
} }
backup[bucketName] = list
return nil return nil
}) })
}); err != nil {
return err
})
if err != nil {
return []byte("{}"), err return []byte("{}"), err
} }

View File

@@ -6,6 +6,7 @@ import (
dserrors "github.com/portainer/portainer/api/dataservices/errors" dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
bolt "go.etcd.io/bbolt" bolt "go.etcd.io/bbolt"
) )
@@ -31,6 +32,33 @@ func (tx *DbTransaction) GetObject(bucketName string, key []byte, object any) er
return tx.conn.UnmarshalObject(value, object) return tx.conn.UnmarshalObject(value, object)
} }
func (tx *DbTransaction) GetRawBytes(bucketName string, key []byte) ([]byte, error) {
bucket := tx.tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
if value == nil {
return nil, fmt.Errorf("%w (bucket=%s, key=%s)", dserrors.ErrObjectNotFound, bucketName, keyToString(key))
}
if tx.conn.getEncryptionKey() != nil {
var err error
if value, err = decrypt(value, tx.conn.getEncryptionKey()); err != nil {
return value, errors.Wrap(err, "Failed decrypting object")
}
}
return value, nil
}
func (tx *DbTransaction) KeyExists(bucketName string, key []byte) (bool, error) {
bucket := tx.tx.Bucket([]byte(bucketName))
value := bucket.Get(key)
return value != nil, nil
}
func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object any) error { func (tx *DbTransaction) UpdateObject(bucketName string, key []byte, object any) error {
data, err := tx.conn.MarshalObject(object) data, err := tx.conn.MarshalObject(object)
if err != nil { if err != nil {

View File

@@ -21,8 +21,7 @@ type Service struct {
// NewService creates a new instance of a service. // NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) { func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName) if err := connection.SetServiceName(BucketName); err != nil {
if err != nil {
return nil, err return nil, err
} }
@@ -62,7 +61,7 @@ func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer
// Note: there is a 1-to-1 mapping of api-key and digest // Note: there is a 1-to-1 mapping of api-key and digest
func (service *Service) GetAPIKeyByDigest(digest string) (*portainer.APIKey, error) { func (service *Service) GetAPIKeyByDigest(digest string) (*portainer.APIKey, error) {
var k *portainer.APIKey var k *portainer.APIKey
stop := fmt.Errorf("ok") stop := errors.New("ok")
err := service.Connection.GetAll( err := service.Connection.GetAll(
BucketName, BucketName,
&portainer.APIKey{}, &portainer.APIKey{},

View File

@@ -9,6 +9,7 @@ import (
type BaseCRUD[T any, I constraints.Integer] interface { type BaseCRUD[T any, I constraints.Integer] interface {
Create(element *T) error Create(element *T) error
Read(ID I) (*T, error) Read(ID I) (*T, error)
Exists(ID I) (bool, error)
ReadAll() ([]T, error) ReadAll() ([]T, error)
Update(ID I, element *T) error Update(ID I, element *T) error
Delete(ID I) error Delete(ID I) error
@@ -42,6 +43,19 @@ func (service BaseDataService[T, I]) Read(ID I) (*T, error) {
}) })
} }
func (service BaseDataService[T, I]) Exists(ID I) (bool, error) {
var exists bool
err := service.Connection.ViewTx(func(tx portainer.Transaction) error {
var err error
exists, err = service.Tx(tx).Exists(ID)
return err
})
return exists, err
}
func (service BaseDataService[T, I]) ReadAll() ([]T, error) { func (service BaseDataService[T, I]) ReadAll() ([]T, error) {
var collection = make([]T, 0) var collection = make([]T, 0)

View File

@@ -28,6 +28,12 @@ func (service BaseDataServiceTx[T, I]) Read(ID I) (*T, error) {
return &element, nil return &element, nil
} }
func (service BaseDataServiceTx[T, I]) Exists(ID I) (bool, error) {
identifier := service.Connection.ConvertToKey(int(ID))
return service.Tx.KeyExists(service.Bucket, identifier)
}
func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) { func (service BaseDataServiceTx[T, I]) ReadAll() ([]T, error) {
var collection = make([]T, 0) var collection = make([]T, 0)

View File

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

View File

@@ -44,8 +44,7 @@ func (service ServiceTx) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeSta
var stack portainer.EdgeStack var stack portainer.EdgeStack
identifier := service.service.connection.ConvertToKey(int(ID)) identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.GetObject(BucketName, identifier, &stack) if err := service.tx.GetObject(BucketName, identifier, &stack); err != nil {
if err != nil {
return nil, err 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 { func (service ServiceTx) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
edgeStack.ID = id edgeStack.ID = id
err := service.tx.CreateObjectWithId( if err := service.tx.CreateObjectWithId(
BucketName, BucketName,
int(edgeStack.ID), int(edgeStack.ID),
edgeStack, edgeStack,
) ); err != nil {
if err != nil {
return err return err
} }
service.service.mu.Lock() service.service.mu.Lock()
service.service.idxVersion[id] = edgeStack.Version service.service.idxVersion[id] = edgeStack.Version
service.service.cacheInvalidationFn(id) service.service.cacheInvalidationFn(service.tx, id)
service.service.mu.Unlock() service.service.mu.Unlock()
return nil return nil
@@ -89,13 +87,12 @@ func (service ServiceTx) UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *po
identifier := service.service.connection.ConvertToKey(int(ID)) identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.UpdateObject(BucketName, identifier, edgeStack) if err := service.tx.UpdateObject(BucketName, identifier, edgeStack); err != nil {
if err != nil {
return err return err
} }
service.service.idxVersion[ID] = edgeStack.Version service.service.idxVersion[ID] = edgeStack.Version
service.service.cacheInvalidationFn(ID) service.service.cacheInvalidationFn(service.tx, ID)
return nil return nil
} }
@@ -119,14 +116,13 @@ func (service ServiceTx) DeleteEdgeStack(ID portainer.EdgeStackID) error {
identifier := service.service.connection.ConvertToKey(int(ID)) identifier := service.service.connection.ConvertToKey(int(ID))
err := service.tx.DeleteObject(BucketName, identifier) if err := service.tx.DeleteObject(BucketName, identifier); err != nil {
if err != nil {
return err return err
} }
delete(service.service.idxVersion, ID) delete(service.service.idxVersion, ID)
service.service.cacheInvalidationFn(ID) service.service.cacheInvalidationFn(service.tx, ID)
return nil return nil
} }

View File

@@ -0,0 +1,89 @@
package edgestackstatus
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
var _ dataservices.EdgeStackStatusService = &Service{}
const BucketName = "edge_stack_status"
type Service struct {
conn portainer.Connection
}
func (service *Service) BucketName() string {
return BucketName
}
func NewService(connection portainer.Connection) (*Service, error) {
if err := connection.SetServiceName(BucketName); err != nil {
return nil, err
}
return &Service{conn: connection}, nil
}
func (s *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: s,
tx: tx,
}
}
func (s *Service) Create(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Create(edgeStackID, endpointID, status)
})
}
func (s *Service) Read(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) (*portainer.EdgeStackStatusForEnv, error) {
var element *portainer.EdgeStackStatusForEnv
return element, s.conn.ViewTx(func(tx portainer.Transaction) error {
var err error
element, err = s.Tx(tx).Read(edgeStackID, endpointID)
return err
})
}
func (s *Service) ReadAll(edgeStackID portainer.EdgeStackID) ([]portainer.EdgeStackStatusForEnv, error) {
var collection = make([]portainer.EdgeStackStatusForEnv, 0)
return collection, s.conn.ViewTx(func(tx portainer.Transaction) error {
var err error
collection, err = s.Tx(tx).ReadAll(edgeStackID)
return err
})
}
func (s *Service) Update(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Update(edgeStackID, endpointID, status)
})
}
func (s *Service) Delete(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Delete(edgeStackID, endpointID)
})
}
func (s *Service) DeleteAll(edgeStackID portainer.EdgeStackID) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).DeleteAll(edgeStackID)
})
}
func (s *Service) Clear(edgeStackID portainer.EdgeStackID, relatedEnvironmentsIDs []portainer.EndpointID) error {
return s.conn.UpdateTx(func(tx portainer.Transaction) error {
return s.Tx(tx).Clear(edgeStackID, relatedEnvironmentsIDs)
})
}
func (s *Service) key(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) []byte {
return append(s.conn.ConvertToKey(int(edgeStackID)), s.conn.ConvertToKey(int(endpointID))...)
}

View File

@@ -0,0 +1,95 @@
package edgestackstatus
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
var _ dataservices.EdgeStackStatusService = &Service{}
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) Create(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
identifier := service.service.key(edgeStackID, endpointID)
return service.tx.CreateObjectWithStringId(BucketName, identifier, status)
}
func (s ServiceTx) Read(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) (*portainer.EdgeStackStatusForEnv, error) {
var status portainer.EdgeStackStatusForEnv
identifier := s.service.key(edgeStackID, endpointID)
if err := s.tx.GetObject(BucketName, identifier, &status); err != nil {
return nil, err
}
return &status, nil
}
func (s ServiceTx) ReadAll(edgeStackID portainer.EdgeStackID) ([]portainer.EdgeStackStatusForEnv, error) {
keyPrefix := s.service.conn.ConvertToKey(int(edgeStackID))
statuses := make([]portainer.EdgeStackStatusForEnv, 0)
if err := s.tx.GetAllWithKeyPrefix(BucketName, keyPrefix, &portainer.EdgeStackStatusForEnv{}, dataservices.AppendFn(&statuses)); err != nil {
return nil, fmt.Errorf("unable to retrieve EdgeStackStatus for EdgeStack %d: %w", edgeStackID, err)
}
return statuses, nil
}
func (s ServiceTx) Update(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error {
identifier := s.service.key(edgeStackID, endpointID)
return s.tx.UpdateObject(BucketName, identifier, status)
}
func (s ServiceTx) Delete(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) error {
identifier := s.service.key(edgeStackID, endpointID)
return s.tx.DeleteObject(BucketName, identifier)
}
func (s ServiceTx) DeleteAll(edgeStackID portainer.EdgeStackID) error {
keyPrefix := s.service.conn.ConvertToKey(int(edgeStackID))
statuses := make([]portainer.EdgeStackStatusForEnv, 0)
if err := s.tx.GetAllWithKeyPrefix(BucketName, keyPrefix, &portainer.EdgeStackStatusForEnv{}, dataservices.AppendFn(&statuses)); err != nil {
return fmt.Errorf("unable to retrieve EdgeStackStatus for EdgeStack %d: %w", edgeStackID, err)
}
for _, status := range statuses {
if err := s.tx.DeleteObject(BucketName, s.service.key(edgeStackID, status.EndpointID)); err != nil {
return fmt.Errorf("unable to delete EdgeStackStatus for EdgeStack %d and Endpoint %d: %w", edgeStackID, status.EndpointID, err)
}
}
return nil
}
func (s ServiceTx) Clear(edgeStackID portainer.EdgeStackID, relatedEnvironmentsIDs []portainer.EndpointID) error {
for _, envID := range relatedEnvironmentsIDs {
existingStatus, err := s.Read(edgeStackID, envID)
if err != nil && !dataservices.IsErrObjectNotFound(err) {
return fmt.Errorf("unable to retrieve status for environment %d: %w", envID, err)
}
var deploymentInfo portainer.StackDeploymentInfo
if existingStatus != nil {
deploymentInfo = existingStatus.DeploymentInfo
}
if err := s.Update(edgeStackID, envID, &portainer.EdgeStackStatusForEnv{
EndpointID: envID,
Status: []portainer.EdgeStackDeploymentStatus{},
DeploymentInfo: deploymentInfo,
}); err != nil {
return err
}
}
return nil
}

View File

@@ -1,6 +1,8 @@
package endpointrelation package endpointrelation
import ( import (
"sync"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/edge/cache" "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. // Service represents a service for managing environment(endpoint) relation data.
type Service struct { type Service struct {
connection portainer.Connection connection portainer.Connection
updateStackFn func(ID portainer.EdgeStackID, updateFunc func(edgeStack *portainer.EdgeStack)) error 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 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 { func (service *Service) BucketName() string {
return BucketName return BucketName
} }
@@ -76,6 +82,10 @@ func (service *Service) Create(endpointRelation *portainer.EndpointRelation) err
err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation) err := service.connection.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
cache.Del(endpointRelation.EndpointID) cache.Del(endpointRelation.EndpointID)
service.mu.Lock()
service.endpointRelationsCache = nil
service.mu.Unlock()
return err return err
} }
@@ -92,11 +102,27 @@ func (service *Service) UpdateEndpointRelation(endpointID portainer.EndpointID,
updatedRelationState, _ := service.EndpointRelation(endpointID) updatedRelationState, _ := service.EndpointRelation(endpointID)
service.mu.Lock()
service.endpointRelationsCache = nil
service.mu.Unlock()
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState) service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
return nil 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 // DeleteEndpointRelation deletes an Environment(Endpoint) relation object
func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error { func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
deletedRelation, _ := service.EndpointRelation(endpointID) deletedRelation, _ := service.EndpointRelation(endpointID)
@@ -108,27 +134,15 @@ func (service *Service) DeleteEndpointRelation(endpointID portainer.EndpointID)
return err return err
} }
service.mu.Lock()
service.endpointRelationsCache = nil
service.mu.Unlock()
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil) service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
return 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) { func (service *Service) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
relations, _ := service.EndpointRelations() relations, _ := service.EndpointRelations()

View File

@@ -13,6 +13,8 @@ type ServiceTx struct {
tx portainer.Transaction tx portainer.Transaction
} }
var _ dataservices.EndpointRelationService = &ServiceTx{}
func (service ServiceTx) BucketName() string { func (service ServiceTx) BucketName() string {
return BucketName return BucketName
} }
@@ -45,6 +47,10 @@ func (service ServiceTx) Create(endpointRelation *portainer.EndpointRelation) er
err := service.tx.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation) err := service.tx.CreateObjectWithId(BucketName, int(endpointRelation.EndpointID), endpointRelation)
cache.Del(endpointRelation.EndpointID) cache.Del(endpointRelation.EndpointID)
service.service.mu.Lock()
service.service.endpointRelationsCache = nil
service.service.mu.Unlock()
return err return err
} }
@@ -61,11 +67,75 @@ func (service ServiceTx) UpdateEndpointRelation(endpointID portainer.EndpointID,
updatedRelationState, _ := service.EndpointRelation(endpointID) updatedRelationState, _ := service.EndpointRelation(endpointID)
service.service.mu.Lock()
service.service.endpointRelationsCache = nil
service.service.mu.Unlock()
service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState) service.updateEdgeStacksAfterRelationChange(previousRelationState, updatedRelationState)
return nil 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
}
}
service.service.mu.Lock()
service.service.endpointRelationsCache = nil
service.service.mu.Unlock()
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
}
}
service.service.mu.Lock()
service.service.endpointRelationsCache = nil
service.service.mu.Unlock()
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 // DeleteEndpointRelation deletes an Environment(Endpoint) relation object
func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID) error { func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID) error {
deletedRelation, _ := service.EndpointRelation(endpointID) deletedRelation, _ := service.EndpointRelation(endpointID)
@@ -77,27 +147,44 @@ func (service ServiceTx) DeleteEndpointRelation(endpointID portainer.EndpointID)
return err return err
} }
service.service.mu.Lock()
service.service.endpointRelationsCache = nil
service.service.mu.Unlock()
service.updateEdgeStacksAfterRelationChange(deletedRelation, nil) service.updateEdgeStacksAfterRelationChange(deletedRelation, nil)
return nil return nil
} }
func (service ServiceTx) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) { func (service ServiceTx) InvalidateEdgeCacheForEdgeStack(edgeStackID portainer.EdgeStackID) {
rels, err := service.EndpointRelations() rels, err := service.cachedEndpointRelations()
if err != nil { if err != nil {
log.Error().Err(err).Msg("cannot retrieve endpoint relations") log.Error().Err(err).Msg("cannot retrieve endpoint relations")
return return
} }
for _, rel := range rels { for _, rel := range rels {
for id := range rel.EdgeStacks { if _, ok := rel.EdgeStacks[edgeStackID]; ok {
if edgeStackID == id { cache.Del(rel.EndpointID)
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) { func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationState *portainer.EndpointRelation, updatedRelationState *portainer.EndpointRelation) {
relations, _ := service.EndpointRelations() relations, _ := service.EndpointRelations()
@@ -133,6 +220,7 @@ func (service ServiceTx) updateEdgeStacksAfterRelationChange(previousRelationSta
} }
numDeployments := 0 numDeployments := 0
for _, r := range relations { for _, r := range relations {
for sId, enabled := range r.EdgeStacks { for sId, enabled := range r.EdgeStacks {
if enabled && sId == refStackId { if enabled && sId == refStackId {

View File

@@ -12,6 +12,7 @@ type (
EdgeGroup() EdgeGroupService EdgeGroup() EdgeGroupService
EdgeJob() EdgeJobService EdgeJob() EdgeJobService
EdgeStack() EdgeStackService EdgeStack() EdgeStackService
EdgeStackStatus() EdgeStackStatusService
Endpoint() EndpointService Endpoint() EndpointService
EndpointGroup() EndpointGroupService EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService EndpointRelation() EndpointRelationService
@@ -39,8 +40,8 @@ type (
Open() (newStore bool, err error) Open() (newStore bool, err error)
Init() error Init() error
Close() error Close() error
UpdateTx(func(DataStoreTx) error) error UpdateTx(func(tx DataStoreTx) error) error
ViewTx(func(DataStoreTx) error) error ViewTx(func(tx DataStoreTx) error) error
MigrateData() error MigrateData() error
Rollback(force bool) error Rollback(force bool) error
CheckCurrentEdition() error CheckCurrentEdition() error
@@ -89,6 +90,16 @@ type (
BucketName() string BucketName() string
} }
EdgeStackStatusService interface {
Create(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error
Read(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) (*portainer.EdgeStackStatusForEnv, error)
ReadAll(edgeStackID portainer.EdgeStackID) ([]portainer.EdgeStackStatusForEnv, error)
Update(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID, status *portainer.EdgeStackStatusForEnv) error
Delete(edgeStackID portainer.EdgeStackID, endpointID portainer.EndpointID) error
DeleteAll(edgeStackID portainer.EdgeStackID) error
Clear(edgeStackID portainer.EdgeStackID, relatedEnvironmentsIDs []portainer.EndpointID) error
}
// EndpointService represents a service for managing environment(endpoint) data // EndpointService represents a service for managing environment(endpoint) data
EndpointService interface { EndpointService interface {
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error) Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
@@ -115,6 +126,8 @@ type (
EndpointRelation(EndpointID portainer.EndpointID) (*portainer.EndpointRelation, error) EndpointRelation(EndpointID portainer.EndpointID) (*portainer.EndpointRelation, error)
Create(endpointRelation *portainer.EndpointRelation) error Create(endpointRelation *portainer.EndpointRelation) error
UpdateEndpointRelation(EndpointID portainer.EndpointID, 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 DeleteEndpointRelation(EndpointID portainer.EndpointID) error
BucketName() string BucketName() string
} }
@@ -157,6 +170,7 @@ type (
SnapshotService interface { SnapshotService interface {
BaseCRUD[portainer.Snapshot, portainer.EndpointID] BaseCRUD[portainer.Snapshot, portainer.EndpointID]
ReadWithoutSnapshotRaw(ID portainer.EndpointID) (*portainer.Snapshot, error)
} }
// SSLSettingsService represents a service for managing application settings // SSLSettingsService represents a service for managing application settings

View File

@@ -48,7 +48,7 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
// if no ResourceControl was found. // if no ResourceControl was found.
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) { func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl var resourceControl *portainer.ResourceControl
stop := fmt.Errorf("ok") stop := errors.New("ok")
err := service.Connection.GetAll( err := service.Connection.GetAll(
BucketName, BucketName,
&portainer.ResourceControl{}, &portainer.ResourceControl{},

View File

@@ -19,7 +19,7 @@ type ServiceTx struct {
// if no ResourceControl was found. // if no ResourceControl was found.
func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) { func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
var resourceControl *portainer.ResourceControl var resourceControl *portainer.ResourceControl
stop := fmt.Errorf("ok") stop := errors.New("ok")
err := service.Tx.GetAll( err := service.Tx.GetAll(
BucketName, BucketName,
&portainer.ResourceControl{}, &portainer.ResourceControl{},

View File

@@ -38,3 +38,33 @@ func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
func (service *Service) Create(snapshot *portainer.Snapshot) error { func (service *Service) Create(snapshot *portainer.Snapshot) error {
return service.Connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot) return service.Connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
} }
func (service *Service) ReadWithoutSnapshotRaw(ID portainer.EndpointID) (*portainer.Snapshot, error) {
var snapshot *portainer.Snapshot
err := service.Connection.ViewTx(func(tx portainer.Transaction) error {
var err error
snapshot, err = service.Tx(tx).ReadWithoutSnapshotRaw(ID)
return err
})
return snapshot, err
}
func (service *Service) ReadRawMessage(ID portainer.EndpointID) (*portainer.SnapshotRawMessage, error) {
var snapshot *portainer.SnapshotRawMessage
err := service.Connection.ViewTx(func(tx portainer.Transaction) error {
var err error
snapshot, err = service.Tx(tx).ReadRawMessage(ID)
return err
})
return snapshot, err
}
func (service *Service) CreateRawMessage(snapshot *portainer.SnapshotRawMessage) error {
return service.Connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
}

View File

@@ -12,3 +12,42 @@ type ServiceTx struct {
func (service ServiceTx) Create(snapshot *portainer.Snapshot) error { func (service ServiceTx) Create(snapshot *portainer.Snapshot) error {
return service.Tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot) return service.Tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
} }
func (service ServiceTx) ReadWithoutSnapshotRaw(ID portainer.EndpointID) (*portainer.Snapshot, error) {
var snapshot struct {
Docker *struct {
X struct{} `json:"DockerSnapshotRaw"`
*portainer.DockerSnapshot
} `json:"Docker"`
portainer.Snapshot
}
identifier := service.Connection.ConvertToKey(int(ID))
if err := service.Tx.GetObject(service.Bucket, identifier, &snapshot); err != nil {
return nil, err
}
if snapshot.Docker != nil {
snapshot.Snapshot.Docker = snapshot.Docker.DockerSnapshot
}
return &snapshot.Snapshot, nil
}
func (service ServiceTx) ReadRawMessage(ID portainer.EndpointID) (*portainer.SnapshotRawMessage, error) {
var snapshot = portainer.SnapshotRawMessage{}
identifier := service.Connection.ConvertToKey(int(ID))
if err := service.Tx.GetObject(service.Bucket, identifier, &snapshot); err != nil {
return nil, err
}
return &snapshot, nil
}
func (service ServiceTx) CreateRawMessage(snapshot *portainer.SnapshotRawMessage) error {
return service.Tx.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
}

View File

@@ -1,7 +1,6 @@
package datastore package datastore
import ( import (
"fmt"
"testing" "testing"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
@@ -33,7 +32,7 @@ func TestStoreCreation(t *testing.T) {
func TestBackup(t *testing.T) { func TestBackup(t *testing.T) {
_, store := MustNewTestStore(t, true, true) _, store := MustNewTestStore(t, true, true)
backupFileName := store.backupFilename() 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{ v := models.Version{
Edition: int(portainer.PortainerCE), Edition: int(portainer.PortainerCE),
SchemaVersion: portainer.APIVersion, SchemaVersion: portainer.APIVersion,

View File

@@ -16,8 +16,9 @@ import (
) )
// NewStore initializes a new Store and the associated services // 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{ return &Store{
flags: cliFlags,
fileService: fileService, fileService: fileService,
connection: connection, connection: connection,
} }

View File

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

View File

@@ -32,7 +32,7 @@ func (store *Store) MigrateData() error {
return errors.Wrap(err, "while migrating legacy version") return errors.Wrap(err, "while migrating legacy version")
} }
migratorParams := store.newMigratorParameters(version) migratorParams := store.newMigratorParameters(version, store.flags)
migrator := migrator.NewMigrator(migratorParams) migrator := migrator.NewMigrator(migratorParams)
if !migrator.NeedsMigration() { if !migrator.NeedsMigration() {
@@ -40,13 +40,11 @@ func (store *Store) MigrateData() error {
} }
// before we alter anything in the DB, create a backup // before we alter anything in the DB, create a backup
_, err = store.Backup("") if _, err := store.Backup(""); err != nil {
if err != nil {
return errors.Wrap(err, "while backing up database") return errors.Wrap(err, "while backing up database")
} }
err = store.FailSafeMigrate(migrator, version) if err := store.FailSafeMigrate(migrator, version); err != nil {
if err != nil {
err = errors.Wrap(err, "failed to migrate database") err = errors.Wrap(err, "failed to migrate database")
log.Warn().Err(err).Msg("migration failed, restoring database to previous version") log.Warn().Err(err).Msg("migration failed, restoring database to previous version")
@@ -62,8 +60,9 @@ func (store *Store) MigrateData() error {
return nil 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{ return &migrator.MigratorParameters{
Flags: flags,
CurrentDBVersion: version, CurrentDBVersion: version,
EndpointGroupService: store.EndpointGroupService, EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService, EndpointService: store.EndpointService,
@@ -84,6 +83,7 @@ func (store *Store) newMigratorParameters(version *models.Version) *migrator.Mig
DockerhubService: store.DockerHubService, DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store), AuthorizationService: authorization.NewService(store),
EdgeStackService: store.EdgeStackService, EdgeStackService: store.EdgeStackService,
EdgeStackStatusService: store.EdgeStackStatusService,
EdgeJobService: store.EdgeJobService, EdgeJobService: store.EdgeJobService,
TunnelServerService: store.TunnelServerService, TunnelServerService: store.TunnelServerService,
PendingActionsService: store.PendingActionsService, PendingActionsService: store.PendingActionsService,
@@ -139,8 +139,7 @@ func (store *Store) connectionRollback(force bool) error {
} }
} }
err := store.Restore() if err := store.Restore(); err != nil {
if err != nil {
return err return err
} }

View File

@@ -109,7 +109,7 @@ func TestMigrateData(t *testing.T) {
t.FailNow() t.FailNow()
} }
migratorParams := store.newMigratorParameters(v) migratorParams := store.newMigratorParameters(v, store.flags)
m := migrator.NewMigrator(migratorParams) m := migrator.NewMigrator(migratorParams)
latestMigrations := m.LatestMigrations() latestMigrations := m.LatestMigrations()

View File

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

View File

@@ -0,0 +1,31 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) migrateEdgeStacksStatuses_2_31_0() error {
edgeStacks, err := m.edgeStackService.EdgeStacks()
if err != nil {
return err
}
for _, edgeStack := range edgeStacks {
for envID, status := range edgeStack.Status {
if err := m.edgeStackStatusService.Create(edgeStack.ID, envID, &portainer.EdgeStackStatusForEnv{
EndpointID: envID,
Status: status.Status,
DeploymentInfo: status.DeploymentInfo,
ReadyRePullImage: status.ReadyRePullImage,
}); err != nil {
return err
}
}
edgeStack.Status = nil
if err := m.edgeStackService.UpdateEdgeStack(edgeStack.ID, &edgeStack); err != nil {
return err
}
}
return nil
}

View File

@@ -94,6 +94,10 @@ func (m *Migrator) updateEdgeStackStatusForDB100() error {
continue continue
} }
if environmentStatus.Details == nil {
continue
}
statusArray := []portainer.EdgeStackDeploymentStatus{} statusArray := []portainer.EdgeStackDeploymentStatus{}
if environmentStatus.Details.Pending { if environmentStatus.Details.Pending {
statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{ statusArray = append(statusArray, portainer.EdgeStackDeploymentStatus{

View File

@@ -18,8 +18,7 @@ func (m *Migrator) updateResourceControlsToDBVersion22() error {
for _, resourceControl := range legacyResourceControls { for _, resourceControl := range legacyResourceControls {
resourceControl.AdministratorsOnly = false resourceControl.AdministratorsOnly = false
err := m.resourceControlService.Update(resourceControl.ID, &resourceControl) if err := m.resourceControlService.Update(resourceControl.ID, &resourceControl); err != nil {
if err != nil {
return err return err
} }
} }
@@ -42,8 +41,8 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
for _, user := range legacyUsers { for _, user := range legacyUsers {
user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations() user.PortainerAuthorizations = authorization.DefaultPortainerAuthorizations()
err = m.userService.Update(user.ID, &user)
if err != nil { if err := m.userService.Update(user.ID, &user); err != nil {
return err return err
} }
} }
@@ -52,38 +51,47 @@ func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
if err != nil { if err != nil {
return err return err
} }
endpointAdministratorRole.Priority = 1 endpointAdministratorRole.Priority = 1
endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole() endpointAdministratorRole.Authorizations = authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
err = m.roleService.Update(endpointAdministratorRole.ID, endpointAdministratorRole) if err := m.roleService.Update(endpointAdministratorRole.ID, endpointAdministratorRole); err != nil {
return err
}
helpDeskRole, err := m.roleService.Read(portainer.RoleID(2)) helpDeskRole, err := m.roleService.Read(portainer.RoleID(2))
if err != nil { if err != nil {
return err return err
} }
helpDeskRole.Priority = 2 helpDeskRole.Priority = 2
helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers) helpDeskRole.Authorizations = authorization.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.Update(helpDeskRole.ID, helpDeskRole) if err := m.roleService.Update(helpDeskRole.ID, helpDeskRole); err != nil {
return err
}
standardUserRole, err := m.roleService.Read(portainer.RoleID(3)) standardUserRole, err := m.roleService.Read(portainer.RoleID(3))
if err != nil { if err != nil {
return err return err
} }
standardUserRole.Priority = 3 standardUserRole.Priority = 3
standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers) standardUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.Update(standardUserRole.ID, standardUserRole) if err := m.roleService.Update(standardUserRole.ID, standardUserRole); err != nil {
return err
}
readOnlyUserRole, err := m.roleService.Read(portainer.RoleID(4)) readOnlyUserRole, err := m.roleService.Read(portainer.RoleID(4))
if err != nil { if err != nil {
return err return err
} }
readOnlyUserRole.Priority = 4 readOnlyUserRole.Priority = 4
readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers) readOnlyUserRole.Authorizations = authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
err = m.roleService.Update(readOnlyUserRole.ID, readOnlyUserRole) if err := m.roleService.Update(readOnlyUserRole.ID, readOnlyUserRole); err != nil {
if err != nil {
return err return err
} }

View File

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

View File

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

View File

@@ -3,12 +3,12 @@ package migrator
import ( import (
"errors" "errors"
"github.com/Masterminds/semver"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models" "github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices/dockerhub" "github.com/portainer/portainer/api/dataservices/dockerhub"
"github.com/portainer/portainer/api/dataservices/edgejob" "github.com/portainer/portainer/api/dataservices/edgejob"
"github.com/portainer/portainer/api/dataservices/edgestack" "github.com/portainer/portainer/api/dataservices/edgestack"
"github.com/portainer/portainer/api/dataservices/edgestackstatus"
"github.com/portainer/portainer/api/dataservices/endpoint" "github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/endpointgroup" "github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation" "github.com/portainer/portainer/api/dataservices/endpointrelation"
@@ -27,12 +27,15 @@ import (
"github.com/portainer/portainer/api/dataservices/user" "github.com/portainer/portainer/api/dataservices/user"
"github.com/portainer/portainer/api/dataservices/version" "github.com/portainer/portainer/api/dataservices/version"
"github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/internal/authorization"
"github.com/Masterminds/semver"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
type ( type (
// Migrator defines a service to migrate data after a Portainer version update. // Migrator defines a service to migrate data after a Portainer version update.
Migrator struct { Migrator struct {
flags *portainer.CLIFlags
currentDBVersion *models.Version currentDBVersion *models.Version
migrations []Migrations migrations []Migrations
@@ -55,6 +58,7 @@ type (
authorizationService *authorization.Service authorizationService *authorization.Service
dockerhubService *dockerhub.Service dockerhubService *dockerhub.Service
edgeStackService *edgestack.Service edgeStackService *edgestack.Service
edgeStackStatusService *edgestackstatus.Service
edgeJobService *edgejob.Service edgeJobService *edgejob.Service
TunnelServerService *tunnelserver.Service TunnelServerService *tunnelserver.Service
pendingActionsService *pendingactions.Service pendingActionsService *pendingactions.Service
@@ -62,6 +66,7 @@ type (
// MigratorParameters represents the required parameters to create a new Migrator instance. // MigratorParameters represents the required parameters to create a new Migrator instance.
MigratorParameters struct { MigratorParameters struct {
Flags *portainer.CLIFlags
CurrentDBVersion *models.Version CurrentDBVersion *models.Version
EndpointGroupService *endpointgroup.Service EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service EndpointService *endpoint.Service
@@ -82,6 +87,7 @@ type (
AuthorizationService *authorization.Service AuthorizationService *authorization.Service
DockerhubService *dockerhub.Service DockerhubService *dockerhub.Service
EdgeStackService *edgestack.Service EdgeStackService *edgestack.Service
EdgeStackStatusService *edgestackstatus.Service
EdgeJobService *edgejob.Service EdgeJobService *edgejob.Service
TunnelServerService *tunnelserver.Service TunnelServerService *tunnelserver.Service
PendingActionsService *pendingactions.Service PendingActionsService *pendingactions.Service
@@ -91,6 +97,7 @@ type (
// NewMigrator creates a new Migrator. // NewMigrator creates a new Migrator.
func NewMigrator(parameters *MigratorParameters) *Migrator { func NewMigrator(parameters *MigratorParameters) *Migrator {
migrator := &Migrator{ migrator := &Migrator{
flags: parameters.Flags,
currentDBVersion: parameters.CurrentDBVersion, currentDBVersion: parameters.CurrentDBVersion,
endpointGroupService: parameters.EndpointGroupService, endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService, endpointService: parameters.EndpointService,
@@ -111,6 +118,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
authorizationService: parameters.AuthorizationService, authorizationService: parameters.AuthorizationService,
dockerhubService: parameters.DockerhubService, dockerhubService: parameters.DockerhubService,
edgeStackService: parameters.EdgeStackService, edgeStackService: parameters.EdgeStackService,
edgeStackStatusService: parameters.EdgeStackStatusService,
edgeJobService: parameters.EdgeJobService, edgeJobService: parameters.EdgeJobService,
TunnelServerService: parameters.TunnelServerService, TunnelServerService: parameters.TunnelServerService,
pendingActionsService: parameters.PendingActionsService, pendingActionsService: parameters.PendingActionsService,
@@ -239,6 +247,8 @@ func (m *Migrator) initMigrations() {
m.migratePendingActionsDataForDB130, m.migratePendingActionsDataForDB130,
) )
m.addMigrations("2.31.0", m.migrateEdgeStacksStatuses_2_31_0)
// Add new migrations above... // Add new migrations above...
// One function per migration, each versions migration funcs in the same file. // One function per migration, each versions migration funcs in the same file.
} }

View File

@@ -11,6 +11,7 @@ import (
"github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/pendingactions/actions" "github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/pkg/endpoints"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -49,17 +50,29 @@ func (postInitMigrator *PostInitMigrator) PostInitMigrate() error {
for _, environment := range environments { for _, environment := range environments {
// edge environments will run after the server starts, in pending actions // edge environments will run after the server starts, in pending actions
if endpointutils.IsEdgeEndpoint(&environment) { if endpoints.IsEdgeEndpoint(&environment) {
log.Info().Msgf("Adding pending action 'PostInitMigrateEnvironment' for environment %d", environment.ID) // Skip edge environments that do not have direct connectivity
err = postInitMigrator.createPostInitMigrationPendingAction(environment.ID) if !endpoints.HasDirectConnectivity(&environment) {
if err != nil { continue
log.Error().Err(err).Msgf("Error creating pending action for environment %d", environment.ID) }
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 { } else {
// non-edge environments will run before the server starts. // Non-edge environments will run before the server starts.
err = postInitMigrator.MigrateEnvironment(&environment) if err := postInitMigrator.MigrateEnvironment(&environment); err != nil {
if err != nil { log.Error().
log.Error().Err(err).Msgf("Error running post-init migrations for non-edge environment %d", environment.ID) Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error running post-init migrations for non-edge environment")
} }
} }

View File

@@ -13,6 +13,7 @@ import (
"github.com/portainer/portainer/api/dataservices/edgegroup" "github.com/portainer/portainer/api/dataservices/edgegroup"
"github.com/portainer/portainer/api/dataservices/edgejob" "github.com/portainer/portainer/api/dataservices/edgejob"
"github.com/portainer/portainer/api/dataservices/edgestack" "github.com/portainer/portainer/api/dataservices/edgestack"
"github.com/portainer/portainer/api/dataservices/edgestackstatus"
"github.com/portainer/portainer/api/dataservices/endpoint" "github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/endpointgroup" "github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation" "github.com/portainer/portainer/api/dataservices/endpointrelation"
@@ -39,9 +40,12 @@ import (
"github.com/segmentio/encoding/json" "github.com/segmentio/encoding/json"
) )
var _ dataservices.DataStore = &Store{}
// Store defines the implementation of portainer.DataStore using // Store defines the implementation of portainer.DataStore using
// BoltDB as the storage system. // BoltDB as the storage system.
type Store struct { type Store struct {
flags *portainer.CLIFlags
connection portainer.Connection connection portainer.Connection
fileService portainer.FileService fileService portainer.FileService
@@ -50,6 +54,7 @@ type Store struct {
EdgeGroupService *edgegroup.Service EdgeGroupService *edgegroup.Service
EdgeJobService *edgejob.Service EdgeJobService *edgejob.Service
EdgeStackService *edgestack.Service EdgeStackService *edgestack.Service
EdgeStackStatusService *edgestackstatus.Service
EndpointGroupService *endpointgroup.Service EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service EndpointRelationService *endpointrelation.Service
@@ -99,13 +104,21 @@ func (store *Store) initServices() error {
} }
store.EndpointRelationService = endpointRelationService 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 { if err != nil {
return err return err
} }
store.EdgeStackService = edgeStackService store.EdgeStackService = edgeStackService
endpointRelationService.RegisterUpdateStackFunction(edgeStackService.UpdateEdgeStackFunc, edgeStackService.UpdateEdgeStackFuncTx) endpointRelationService.RegisterUpdateStackFunction(edgeStackService.UpdateEdgeStackFunc, edgeStackService.UpdateEdgeStackFuncTx)
edgeStackStatusService, err := edgestackstatus.NewService(store.connection)
if err != nil {
return err
}
store.EdgeStackStatusService = edgeStackStatusService
edgeGroupService, err := edgegroup.NewService(store.connection) edgeGroupService, err := edgegroup.NewService(store.connection)
if err != nil { if err != nil {
return err return err
@@ -266,6 +279,10 @@ func (store *Store) EdgeStack() dataservices.EdgeStackService {
return store.EdgeStackService return store.EdgeStackService
} }
func (store *Store) EdgeStackStatus() dataservices.EdgeStackStatusService {
return store.EdgeStackStatusService
}
// Environment(Endpoint) gives access to the Environment(Endpoint) data management layer // Environment(Endpoint) gives access to the Environment(Endpoint) data management layer
func (store *Store) Endpoint() dataservices.EndpointService { func (store *Store) Endpoint() dataservices.EndpointService {
return store.EndpointService return store.EndpointService

View File

@@ -32,6 +32,10 @@ func (tx *StoreTx) EdgeStack() dataservices.EdgeStackService {
return tx.store.EdgeStackService.Tx(tx.tx) return tx.store.EdgeStackService.Tx(tx.tx)
} }
func (tx *StoreTx) EdgeStackStatus() dataservices.EdgeStackStatusService {
return tx.store.EdgeStackStatusService.Tx(tx.tx)
}
func (tx *StoreTx) Endpoint() dataservices.EndpointService { func (tx *StoreTx) Endpoint() dataservices.EndpointService {
return tx.store.EndpointService.Tx(tx.tx) return tx.store.EndpointService.Tx(tx.tx)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,16 @@
{ {
"api_key": null,
"customtemplates": null,
"dockerhub": [ "dockerhub": [
{ {
"Authentication": false, "Authentication": false,
"Username": "" "Username": ""
} }
], ],
"edge_stack": null,
"edge_stack_status": null,
"edgegroups": null,
"edgejobs": null,
"endpoint_groups": [ "endpoint_groups": [
{ {
"AuthorizedTeams": null, "AuthorizedTeams": null,
@@ -103,6 +109,9 @@
"UserAccessPolicies": {} "UserAccessPolicies": {}
} }
], ],
"extension": null,
"helm_user_repository": null,
"pending_actions": null,
"registries": [ "registries": [
{ {
"Authentication": true, "Authentication": true,
@@ -602,7 +611,7 @@
"RequiredPasswordLength": 12 "RequiredPasswordLength": 12
}, },
"KubeconfigExpiry": "0", "KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.22.0", "KubectlShellImage": "portainer/kubectl-shell:2.31.1",
"LDAPSettings": { "LDAPSettings": {
"AnonymousMode": true, "AnonymousMode": true,
"AutoCreateUsers": true, "AutoCreateUsers": true,
@@ -664,19 +673,17 @@
{ {
"Docker": { "Docker": {
"ContainerCount": 0, "ContainerCount": 0,
"DiagnosticsData": {},
"DockerSnapshotRaw": { "DockerSnapshotRaw": {
"Containers": null, "Containers": null,
"Images": null, "Images": null,
"Info": { "Info": {
"Architecture": "", "Architecture": "",
"BridgeNfIp6tables": false,
"BridgeNfIptables": false,
"CDISpecDirs": null, "CDISpecDirs": null,
"CPUSet": false, "CPUSet": false,
"CPUShares": false, "CPUShares": false,
"CgroupDriver": "", "CgroupDriver": "",
"ContainerdCommit": { "ContainerdCommit": {
"Expected": "",
"ID": "" "ID": ""
}, },
"Containers": 0, "Containers": 0,
@@ -700,7 +707,6 @@
"IndexServerAddress": "", "IndexServerAddress": "",
"InitBinary": "", "InitBinary": "",
"InitCommit": { "InitCommit": {
"Expected": "",
"ID": "" "ID": ""
}, },
"Isolation": "", "Isolation": "",
@@ -729,7 +735,6 @@
}, },
"RegistryConfig": null, "RegistryConfig": null,
"RuncCommit": { "RuncCommit": {
"Expected": "",
"ID": "" "ID": ""
}, },
"Runtimes": null, "Runtimes": null,
@@ -860,6 +865,8 @@
"UpdatedBy": "" "UpdatedBy": ""
} }
], ],
"tags": null,
"team_membership": null,
"teams": [ "teams": [
{ {
"Id": 1, "Id": 1,
@@ -932,6 +939,7 @@
} }
], ],
"version": { "version": {
"VERSION": "{\"SchemaVersion\":\"2.22.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" "VERSION": "{\"SchemaVersion\":\"2.31.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
} },
"webhooks": null
} }

View File

@@ -29,6 +29,10 @@ func MustNewTestStore(t testing.TB, init, secure bool) (bool, *Store) {
func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error) { func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner. // Creates unique temp directory in a concurrency friendly manner.
storePath := t.TempDir() storePath := t.TempDir()
defaultKubectlShellImage := portainer.DefaultKubectlShellImage
flags := &portainer.CLIFlags{
KubectlShellImage: &defaultKubectlShellImage,
}
fileService, err := filesystem.NewService(storePath, "") fileService, err := filesystem.NewService(storePath, "")
if err != nil { if err != nil {
@@ -45,7 +49,7 @@ func NewTestStore(t testing.TB, init, secure bool) (bool, *Store, func(), error)
panic(err) panic(err)
} }
store := NewStore(storePath, fileService, connection) store := NewStore(flags, fileService, connection)
newStore, err := store.Open() newStore, err := store.Open()
if err != nil { if err != nil {
return newStore, nil, nil, err return newStore, nil, nil, err

View File

@@ -1,15 +0,0 @@
package validate
import (
"github.com/go-playground/validator/v10"
portainer "github.com/portainer/portainer/api"
)
var validate *validator.Validate
func ValidateLDAPSettings(ldp *portainer.LDAPSettings) error {
validate = validator.New()
registerValidationMethods(validate)
return validate.Struct(ldp)
}

View File

@@ -1,61 +0,0 @@
package validate
import (
"testing"
portainer "github.com/portainer/portainer/api"
)
func TestValidateLDAPSettings(t *testing.T) {
tests := []struct {
name string
ldap portainer.LDAPSettings
wantErr bool
}{
{
name: "Empty LDAP Settings",
ldap: portainer.LDAPSettings{},
wantErr: true,
},
{
name: "With URL",
ldap: portainer.LDAPSettings{
AnonymousMode: true,
URL: "192.168.0.1:323",
},
wantErr: false,
},
{
name: "Validate URL and URLs",
ldap: portainer.LDAPSettings{
AnonymousMode: true,
URL: "192.168.0.1:323",
},
wantErr: false,
},
{
name: "validate client ldap",
ldap: portainer.LDAPSettings{
AnonymousMode: false,
ReaderDN: "CN=LDAP API Service Account",
Password: "Qu**dfUUU**",
URL: "aukdc15.pgc.co:389",
TLSConfig: portainer.TLSConfiguration{
TLS: false,
TLSSkipVerify: false,
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateLDAPSettings(&tt.ldap)
if (err == nil) == tt.wantErr {
t.Errorf("No error expected but got %s", err)
}
})
}
}

View File

@@ -1,17 +0,0 @@
package validate
import (
"github.com/go-playground/validator/v10"
)
func registerValidationMethods(v *validator.Validate) {
v.RegisterValidation("validate_bool", ValidateBool)
}
/**
* Validation methods below are being used for custom validation
*/
func ValidateBool(fl validator.FieldLevel) bool {
_, ok := fl.Field().Interface().(bool)
return ok
}

View File

@@ -3,8 +3,8 @@ package client
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"io" "io"
"maps"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -73,19 +73,6 @@ func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
) )
} }
func CreateClientFromEnv() (*client.Client, error) {
return client.NewClientWithOpts(
client.FromEnv,
client.WithAPIVersionNegotiation(),
)
}
func CreateSimpleClient() (*client.Client, error) {
return client.NewClientWithOpts(
client.WithAPIVersionNegotiation(),
)
}
func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*client.Client, error) { func createTCPClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*client.Client, error) {
httpCli, err := httpClient(endpoint, timeout) httpCli, err := httpClient(endpoint, timeout)
if err != nil { if err != nil {
@@ -141,7 +128,6 @@ func createAgentClient(endpoint *portainer.Endpoint, endpointURL string, signatu
type NodeNameTransport struct { type NodeNameTransport struct {
*http.Transport *http.Transport
nodeNames map[string]string
} }
func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error) { func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -176,18 +162,19 @@ func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error)
return resp, nil return resp, nil
} }
t.nodeNames = make(map[string]string) nodeNames, ok := req.Context().Value("nodeNames").(map[string]string)
for _, r := range rs { if ok {
t.nodeNames[r.ID] = r.Portainer.Agent.NodeName for idx, r := range rs {
// as there is no way to differentiate the same image available in multiple nodes only by their ID
// we append the index of the image in the payload response to match the node name later
// from the image.Summary[] list returned by docker's client.ImageList()
nodeNames[fmt.Sprintf("%s-%d", r.ID, idx)] = r.Portainer.Agent.NodeName
}
} }
return resp, err return resp, err
} }
func (t *NodeNameTransport) NodeNames() map[string]string {
return maps.Clone(t.nodeNames)
}
func httpClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*http.Client, error) { func httpClient(endpoint *portainer.Endpoint, timeout *time.Duration) (*http.Client, error) {
transport := &NodeNameTransport{ transport := &NodeNameTransport{
Transport: &http.Transport{}, Transport: &http.Transport{},

View File

@@ -38,10 +38,10 @@ func NewClientWithRegistry(registryClient *RegistryClient, clientFactory *docker
func (c *DigestClient) RemoteDigest(image Image) (digest.Digest, error) { func (c *DigestClient) RemoteDigest(image Image) (digest.Digest, error) {
ctx, cancel := c.timeoutContext() ctx, cancel := c.timeoutContext()
defer cancel() defer cancel()
// Docker references with both a tag and digest are currently not supported // Docker references with both a tag and digest are currently not supported
if image.Tag != "" && image.Digest != "" { if image.Tag != "" && image.Digest != "" {
err := image.trimDigest() if err := image.TrimDigest(); err != nil {
if err != nil {
return "", err return "", err
} }
} }
@@ -69,7 +69,7 @@ func (c *DigestClient) RemoteDigest(image Image) (digest.Digest, error) {
// Retrieve remote digest through HEAD request // Retrieve remote digest through HEAD request
rmDigest, err := docker.GetDigest(ctx, sysCtx, rmRef) rmDigest, err := docker.GetDigest(ctx, sysCtx, rmRef)
if err != nil { if err != nil {
// fallback to public registry for hub // Fallback to public registry for hub
if image.HubLink != "" { if image.HubLink != "" {
rmDigest, err = docker.GetDigest(ctx, c.sysCtx, rmRef) rmDigest, err = docker.GetDigest(ctx, c.sysCtx, rmRef)
if err == nil { if err == nil {
@@ -131,8 +131,7 @@ func ParseRepoDigests(repoDigests []string) []digest.Digest {
func ParseRepoTags(repoTags []string) []*Image { func ParseRepoTags(repoTags []string) []*Image {
images := make([]*Image, 0) images := make([]*Image, 0)
for _, repoTag := range repoTags { for _, repoTag := range repoTags {
image := ParseRepoTag(repoTag) if image := ParseRepoTag(repoTag); image != nil {
if image != nil {
images = append(images, image) images = append(images, image)
} }
} }
@@ -147,7 +146,7 @@ func ParseRepoDigest(repoDigest string) digest.Digest {
d, err := digest.Parse(strings.Split(repoDigest, "@")[1]) d, err := digest.Parse(strings.Split(repoDigest, "@")[1])
if err != nil { if err != nil {
log.Warn().Msgf("Skip invalid repo digest item: %s [error: %v]", repoDigest, err) log.Warn().Err(err).Str("digest", repoDigest).Msg("skip invalid repo item")
return "" return ""
} }

View File

@@ -26,7 +26,7 @@ type Image struct {
Digest digest.Digest Digest digest.Digest
HubLink string HubLink string
named reference.Named named reference.Named
opts ParseImageOptions Opts ParseImageOptions `json:"-"`
} }
// ParseImageOptions holds image options for parsing. // ParseImageOptions holds image options for parsing.
@@ -43,9 +43,10 @@ func (i *Image) Name() string {
// FullName return the real full name may include Tag or Digest of the image, Tag first. // FullName return the real full name may include Tag or Digest of the image, Tag first.
func (i *Image) FullName() string { func (i *Image) FullName() string {
if i.Tag == "" { if i.Tag == "" {
return fmt.Sprintf("%s@%s", i.Name(), i.Digest) return i.Name() + "@" + i.Digest.String()
} }
return fmt.Sprintf("%s:%s", i.Name(), i.Tag)
return i.Name() + ":" + i.Tag
} }
// String returns the string representation of an image, including Tag and Digest if existed. // String returns the string representation of an image, including Tag and Digest if existed.
@@ -66,22 +67,25 @@ func (i *Image) Reference() string {
func (i *Image) WithDigest(digest digest.Digest) (err error) { func (i *Image) WithDigest(digest digest.Digest) (err error) {
i.Digest = digest i.Digest = digest
i.named, err = reference.WithDigest(i.named, digest) i.named, err = reference.WithDigest(i.named, digest)
return err return err
} }
func (i *Image) WithTag(tag string) (err error) { func (i *Image) WithTag(tag string) (err error) {
i.Tag = tag i.Tag = tag
i.named, err = reference.WithTag(i.named, tag) i.named, err = reference.WithTag(i.named, tag)
return err return err
} }
func (i *Image) trimDigest() error { func (i *Image) TrimDigest() error {
i.Digest = "" i.Digest = ""
named, err := ParseImage(ParseImageOptions{Name: i.FullName()}) named, err := ParseImage(ParseImageOptions{Name: i.FullName()})
if err != nil { if err != nil {
return err return err
} }
i.named = &named i.named = &named
return nil return nil
} }
@@ -92,11 +96,12 @@ func ParseImage(parseOpts ParseImageOptions) (Image, error) {
if err != nil { if err != nil {
return Image{}, errors.Wrapf(err, "parsing image %s failed", parseOpts.Name) return Image{}, errors.Wrapf(err, "parsing image %s failed", parseOpts.Name)
} }
// Add the latest lag if they did not provide one. // Add the latest lag if they did not provide one.
named = reference.TagNameOnly(named) named = reference.TagNameOnly(named)
i := Image{ i := Image{
opts: parseOpts, Opts: parseOpts,
named: named, named: named,
Domain: reference.Domain(named), Domain: reference.Domain(named),
Path: reference.Path(named), Path: reference.Path(named),
@@ -122,15 +127,16 @@ func ParseImage(parseOpts ParseImageOptions) (Image, error) {
} }
func (i *Image) hubLink() (string, error) { func (i *Image) hubLink() (string, error) {
if i.opts.HubTpl != "" { if i.Opts.HubTpl != "" {
var out bytes.Buffer var out bytes.Buffer
tmpl, err := template.New("tmpl"). tmpl, err := template.New("tmpl").
Option("missingkey=error"). Option("missingkey=error").
Parse(i.opts.HubTpl) Parse(i.Opts.HubTpl)
if err != nil { if err != nil {
return "", err return "", err
} }
err = tmpl.Execute(&out, i) err = tmpl.Execute(&out, i)
return out.String(), err return out.String(), err
} }
@@ -142,23 +148,24 @@ func (i *Image) hubLink() (string, error) {
prefix = "_" prefix = "_"
path = strings.Replace(i.Path, "library/", "", 1) path = strings.Replace(i.Path, "library/", "", 1)
} }
return fmt.Sprintf("https://hub.docker.com/%s/%s", prefix, path), nil
return "https://hub.docker.com/" + prefix + "/" + path, nil
case "docker.bintray.io", "jfrog-docker-reg2.bintray.io": case "docker.bintray.io", "jfrog-docker-reg2.bintray.io":
return fmt.Sprintf("https://bintray.com/jfrog/reg2/%s", strings.ReplaceAll(i.Path, "/", "%3A")), nil return "https://bintray.com/jfrog/reg2/" + strings.ReplaceAll(i.Path, "/", "%3A"), nil
case "docker.pkg.github.com": case "docker.pkg.github.com":
return fmt.Sprintf("https://github.com/%s/packages", filepath.ToSlash(filepath.Dir(i.Path))), nil return "https://github.com/" + filepath.ToSlash(filepath.Dir(i.Path)) + "/packages", nil
case "gcr.io": case "gcr.io":
return fmt.Sprintf("https://%s/%s", i.Domain, i.Path), nil return "https://" + i.Domain + "/" + i.Path, nil
case "ghcr.io": case "ghcr.io":
ref := strings.Split(i.Path, "/") ref := strings.Split(i.Path, "/")
ghUser, ghPackage := ref[0], ref[1] ghUser, ghPackage := ref[0], ref[1]
return fmt.Sprintf("https://github.com/users/%s/packages/container/package/%s", ghUser, ghPackage), nil return "https://github.com/users/" + ghUser + "/packages/container/package/" + ghPackage, nil
case "quay.io": case "quay.io":
return fmt.Sprintf("https://quay.io/repository/%s", i.Path), nil return "https://quay.io/repository/" + i.Path, nil
case "registry.access.redhat.com": case "registry.access.redhat.com":
return fmt.Sprintf("https://access.redhat.com/containers/#/registry.access.redhat.com/%s", i.Path), nil return "https://access.redhat.com/containers/#/registry.access.redhat.com/" + i.Path, nil
case "registry.gitlab.com": case "registry.gitlab.com":
return fmt.Sprintf("https://gitlab.com/%s/container_registry", i.Path), nil return "https://gitlab.com/" + i.Path + "/container_registry", nil
default: default:
return "", nil return "", nil
} }

View File

@@ -16,7 +16,7 @@ func TestImageParser(t *testing.T) {
}) })
is.NoError(err, "") is.NoError(err, "")
is.Equal("docker.io/portainer/portainer-ee:latest", image.FullName()) is.Equal("docker.io/portainer/portainer-ee:latest", image.FullName())
is.Equal("portainer/portainer-ee", image.opts.Name) is.Equal("portainer/portainer-ee", image.Opts.Name)
is.Equal("latest", image.Tag) is.Equal("latest", image.Tag)
is.Equal("portainer/portainer-ee", image.Path) is.Equal("portainer/portainer-ee", image.Path)
is.Equal("docker.io", image.Domain) is.Equal("docker.io", image.Domain)
@@ -32,7 +32,7 @@ func TestImageParser(t *testing.T) {
}) })
is.NoError(err, "") is.NoError(err, "")
is.Equal("gcr.io/k8s-minikube/kicbase@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.FullName()) is.Equal("gcr.io/k8s-minikube/kicbase@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.FullName())
is.Equal("gcr.io/k8s-minikube/kicbase@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name) is.Equal("gcr.io/k8s-minikube/kicbase@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
is.Equal("", image.Tag) is.Equal("", image.Tag)
is.Equal("k8s-minikube/kicbase", image.Path) is.Equal("k8s-minikube/kicbase", image.Path)
is.Equal("gcr.io", image.Domain) is.Equal("gcr.io", image.Domain)
@@ -49,7 +49,7 @@ func TestImageParser(t *testing.T) {
}) })
is.NoError(err, "") is.NoError(err, "")
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30", image.FullName()) is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30", image.FullName())
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name) is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
is.Equal("v0.0.30", image.Tag) is.Equal("v0.0.30", image.Tag)
is.Equal("k8s-minikube/kicbase", image.Path) is.Equal("k8s-minikube/kicbase", image.Path)
is.Equal("gcr.io", image.Domain) is.Equal("gcr.io", image.Domain)
@@ -71,7 +71,7 @@ func TestUpdateParsedImage(t *testing.T) {
is.NoError(err, "") is.NoError(err, "")
_ = image.WithTag("v0.0.31") _ = image.WithTag("v0.0.31")
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.31", image.FullName()) is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.31", image.FullName())
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name) is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
is.Equal("v0.0.31", image.Tag) is.Equal("v0.0.31", image.Tag)
is.Equal("k8s-minikube/kicbase", image.Path) is.Equal("k8s-minikube/kicbase", image.Path)
is.Equal("gcr.io", image.Domain) is.Equal("gcr.io", image.Domain)
@@ -89,7 +89,7 @@ func TestUpdateParsedImage(t *testing.T) {
is.NoError(err, "") is.NoError(err, "")
_ = image.WithDigest("sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b3") _ = image.WithDigest("sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b3")
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30", image.FullName()) is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30", image.FullName())
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name) is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
is.Equal("v0.0.30", image.Tag) is.Equal("v0.0.30", image.Tag)
is.Equal("k8s-minikube/kicbase", image.Path) is.Equal("k8s-minikube/kicbase", image.Path)
is.Equal("gcr.io", image.Domain) is.Equal("gcr.io", image.Domain)
@@ -105,9 +105,9 @@ func TestUpdateParsedImage(t *testing.T) {
Name: "gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", Name: "gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2",
}) })
is.NoError(err, "") is.NoError(err, "")
_ = image.trimDigest() _ = image.TrimDigest()
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30", image.FullName()) is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30", image.FullName())
is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.opts.Name) is.Equal("gcr.io/k8s-minikube/kicbase:v0.0.30@sha256:02c921df998f95e849058af14de7045efc3954d90320967418a0d1f182bbc0b2", image.Opts.Name)
is.Equal("v0.0.30", image.Tag) is.Equal("v0.0.30", image.Tag)
is.Equal("k8s-minikube/kicbase", image.Path) is.Equal("k8s-minikube/kicbase", image.Path)
is.Equal("gcr.io", image.Domain) is.Equal("gcr.io", image.Domain)

View File

@@ -6,7 +6,7 @@ import (
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types/image"
"github.com/docker/docker/client" "github.com/docker/docker/client"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -25,18 +25,18 @@ func NewPuller(client *client.Client, registryClient *RegistryClient, dataStore
} }
} }
func (puller *Puller) Pull(ctx context.Context, image Image) error { func (puller *Puller) Pull(ctx context.Context, img Image) error {
log.Debug().Str("image", image.FullName()).Msg("starting to pull the image") log.Debug().Str("image", img.FullName()).Msg("starting to pull the image")
registryAuth, err := puller.registryClient.EncodedRegistryAuth(image) registryAuth, err := puller.registryClient.EncodedRegistryAuth(img)
if err != nil { if err != nil {
log.Debug(). log.Debug().
Str("image", image.FullName()). Str("image", img.FullName()).
Err(err). Err(err).
Msg("failed to get an encoded registry auth via image, try to pull image without registry auth") Msg("failed to get an encoded registry auth via image, try to pull image without registry auth")
} }
out, err := puller.client.ImagePull(ctx, image.FullName(), types.ImagePullOptions{ out, err := puller.client.ImagePull(ctx, img.FullName(), image.PullOptions{
RegistryAuth: registryAuth, RegistryAuth: registryAuth,
}) })
if err != nil { if err != nil {

View File

@@ -1,7 +1,6 @@
package images package images
import ( import (
"fmt"
"strings" "strings"
"github.com/containers/image/v5/docker" "github.com/containers/image/v5/docker"
@@ -10,7 +9,7 @@ import (
func ParseReference(imageStr string) (types.ImageReference, error) { func ParseReference(imageStr string) (types.ImageReference, error) {
if !strings.HasPrefix(imageStr, "//") { if !strings.HasPrefix(imageStr, "//") {
imageStr = fmt.Sprintf("//%s", imageStr) imageStr = "//" + imageStr
} }
return docker.ParseReference(imageStr) return docker.ParseReference(imageStr)
} }

View File

@@ -29,7 +29,7 @@ func (c *RegistryClient) RegistryAuth(image Image) (string, string, error) {
return "", "", err return "", "", err
} }
registry, err := findBestMatchRegistry(image.opts.Name, registries) registry, err := findBestMatchRegistry(image.Opts.Name, registries)
if err != nil { if err != nil {
return "", "", err return "", "", err
} }
@@ -59,7 +59,7 @@ func (c *RegistryClient) EncodedRegistryAuth(image Image) (string, error) {
return "", err return "", err
} }
registry, err := findBestMatchRegistry(image.opts.Name, registries) registry, err := findBestMatchRegistry(image.Opts.Name, registries)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -1,20 +1,9 @@
package docker package docker
import ( import (
"context"
"strings"
"time"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
dockerclient "github.com/portainer/portainer/api/docker/client" dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/docker/consts" "github.com/portainer/portainer/pkg/snapshot"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
_container "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/rs/zerolog/log"
) )
// Snapshotter represents a service used to create environment(endpoint) snapshots // Snapshotter represents a service used to create environment(endpoint) snapshots
@@ -37,247 +26,5 @@ func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*p
} }
defer cli.Close() defer cli.Close()
return snapshot(cli, endpoint) return snapshot.CreateDockerSnapshot(cli)
}
func snapshot(cli *client.Client, endpoint *portainer.Endpoint) (*portainer.DockerSnapshot, error) {
if _, err := cli.Ping(context.Background()); err != nil {
return nil, err
}
snapshot := &portainer.DockerSnapshot{
StackCount: 0,
}
if err := snapshotInfo(snapshot, cli); err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot engine information")
}
if snapshot.Swarm {
if err := snapshotSwarmServices(snapshot, cli); err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot Swarm services")
}
if err := snapshotNodes(snapshot, cli); err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot Swarm nodes")
}
}
if err := snapshotContainers(snapshot, cli); err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot containers")
}
if err := snapshotImages(snapshot, cli); err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot images")
}
if err := snapshotVolumes(snapshot, cli); err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot volumes")
}
if err := snapshotNetworks(snapshot, cli); err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot networks")
}
if err := snapshotVersion(snapshot, cli); err != nil {
log.Warn().Str("environment", endpoint.Name).Err(err).Msg("unable to snapshot engine version")
}
snapshot.Time = time.Now().Unix()
return snapshot, nil
}
func snapshotInfo(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
info, err := cli.Info(context.Background())
if err != nil {
return err
}
snapshot.Swarm = info.Swarm.ControlAvailable
snapshot.DockerVersion = info.ServerVersion
snapshot.TotalCPU = info.NCPU
snapshot.TotalMemory = info.MemTotal
snapshot.SnapshotRaw.Info = info
return nil
}
func snapshotNodes(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
nodes, err := cli.NodeList(context.Background(), types.NodeListOptions{})
if err != nil {
return err
}
var nanoCpus int64
var totalMem int64
for _, node := range nodes {
nanoCpus += node.Description.Resources.NanoCPUs
totalMem += node.Description.Resources.MemoryBytes
}
snapshot.TotalCPU = int(nanoCpus / 1e9)
snapshot.TotalMemory = totalMem
snapshot.NodeCount = len(nodes)
return nil
}
func snapshotSwarmServices(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
stacks := make(map[string]struct{})
services, err := cli.ServiceList(context.Background(), types.ServiceListOptions{})
if err != nil {
return err
}
for _, service := range services {
for k, v := range service.Spec.Labels {
if k == "com.docker.stack.namespace" {
stacks[v] = struct{}{}
}
}
}
snapshot.ServiceCount = len(services)
snapshot.StackCount += len(stacks)
return nil
}
func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
containers, err := cli.ContainerList(context.Background(), container.ListOptions{All: true})
if err != nil {
return err
}
stacks := make(map[string]struct{})
gpuUseSet := make(map[string]struct{})
gpuUseAll := false
for _, container := range containers {
if container.State == "running" {
// Snapshot GPUs
response, err := cli.ContainerInspect(context.Background(), container.ID)
if err != nil {
// Inspect a container will fail when the container runs on a different
// Swarm node, so it is better to log the error instead of return error
// when the Swarm mode is enabled
if !snapshot.Swarm {
return err
} else {
if !strings.Contains(err.Error(), "No such container") {
return err
}
// It is common to have containers running on different Swarm nodes,
// so we just log the error in the debug level
log.Debug().Str("container", container.ID).Err(err).Msg("unable to inspect container in other Swarm nodes")
}
} else {
var gpuOptions *_container.DeviceRequest = nil
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
gpuOptions = &deviceRequest
}
}
if gpuOptions != nil {
if gpuOptions.Count == -1 {
gpuUseAll = true
}
for _, id := range gpuOptions.DeviceIDs {
gpuUseSet[id] = struct{}{}
}
}
}
}
for k, v := range container.Labels {
if k == consts.ComposeStackNameLabel {
stacks[v] = struct{}{}
}
}
}
gpuUseList := make([]string, 0, len(gpuUseSet))
for gpuUse := range gpuUseSet {
gpuUseList = append(gpuUseList, gpuUse)
}
snapshot.GpuUseAll = gpuUseAll
snapshot.GpuUseList = gpuUseList
stats := CalculateContainerStats(containers)
snapshot.ContainerCount = stats.Total
snapshot.RunningContainerCount = stats.Running
snapshot.StoppedContainerCount = stats.Stopped
snapshot.HealthyContainerCount = stats.Healthy
snapshot.UnhealthyContainerCount = stats.Unhealthy
snapshot.StackCount += len(stacks)
for _, container := range containers {
snapshot.SnapshotRaw.Containers = append(snapshot.SnapshotRaw.Containers, portainer.DockerContainerSnapshot{Container: container})
}
return nil
}
func snapshotImages(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
images, err := cli.ImageList(context.Background(), types.ImageListOptions{})
if err != nil {
return err
}
snapshot.ImageCount = len(images)
snapshot.SnapshotRaw.Images = images
return nil
}
func snapshotVolumes(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
volumes, err := cli.VolumeList(context.Background(), volume.ListOptions{})
if err != nil {
return err
}
snapshot.VolumeCount = len(volumes.Volumes)
snapshot.SnapshotRaw.Volumes = volumes
return nil
}
func snapshotNetworks(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
networks, err := cli.NetworkList(context.Background(), types.NetworkListOptions{})
if err != nil {
return err
}
snapshot.SnapshotRaw.Networks = networks
return nil
}
func snapshotVersion(snapshot *portainer.DockerSnapshot, cli *client.Client) error {
version, err := cli.ServerVersion(context.Background())
if err != nil {
return err
}
snapshot.SnapshotRaw.Version = version
snapshot.IsPodman = isPodman(version)
return nil
}
// isPodman checks if the version is for Podman by checking if any of the components contain "podman".
// If it's podman, a component name should be "Podman Engine"
func isPodman(version types.Version) bool {
for _, component := range version.Components {
if strings.Contains(strings.ToLower(component.Name), "podman") {
return true
}
}
return false
} }

View File

@@ -31,15 +31,18 @@ type (
// RegistryCredentials holds the credentials for a Docker registry. // RegistryCredentials holds the credentials for a Docker registry.
// Used only for EE // Used only for EE
RegistryCredentials []RegistryCredentials RegistryCredentials []RegistryCredentials
// PrePullImage is a flag indicating if the agent should pull the image before deploying the stack. // PrePullImage is a flag indicating if the agent must pull the image before deploying the stack.
// Used only for EE // Used only for EE
PrePullImage bool PrePullImage bool
// RePullImage is a flag indicating if the agent should pull the image if it is already present on the node. // RePullImage is a flag indicating if the agent must pull the image if it is already present on the node.
// Used only for EE // Used only for EE
RePullImage bool RePullImage bool
// RetryDeploy is a flag indicating if the agent should retry to deploy the stack if it fails. // RetryDeploy is a flag indicating if the agent must retry to deploy the stack if it fails.
// Used only for EE // Used only for EE
RetryDeploy bool RetryDeploy bool
// RetryPeriod specifies the duration, in seconds, for which the agent should continue attempting to deploy the stack after a failure
// Used only for EE
RetryPeriod int
// EdgeUpdateID is the ID of the edge update related to this stack. // EdgeUpdateID is the ID of the edge update related to this stack.
// Used only for EE // Used only for EE
EdgeUpdateID int EdgeUpdateID int
@@ -55,6 +58,20 @@ type (
// Used only for EE async edge agent // Used only for EE async edge agent
// ReadyRePullImage is a flag to indicate whether the auto update is trigger to re-pull image // ReadyRePullImage is a flag to indicate whether the auto update is trigger to re-pull image
ReadyRePullImage bool ReadyRePullImage bool
DeployerOptionsPayload DeployerOptionsPayload
}
DeployerOptionsPayload struct {
// Prune is a flag indicating if the agent must prune the containers or not when creating/updating an edge stack
// This flag drives `docker compose up --remove-orphans` and `docker stack up --prune` options
// Used only for EE
Prune bool
// RemoveVolumes is a flag indicating if the agent must remove the named volumes declared
// in the compose file and anonymouse volumes attached to containers
// This flag drives `docker compose down --volumes` option
// Used only for EE
RemoveVolumes bool
} }
// RegistryCredentials holds the credentials for a Docker registry. // RegistryCredentials holds the credentials for a Docker registry.

View File

@@ -9,27 +9,32 @@ import (
"strings" "strings"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory" "github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/stacks/stackutils" "github.com/portainer/portainer/api/stacks/stackutils"
"github.com/portainer/portainer/pkg/libstack" "github.com/portainer/portainer/pkg/libstack"
"github.com/docker/cli/cli/config/types"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog/log"
) )
// ComposeStackManager is a wrapper for docker-compose binary // ComposeStackManager is a wrapper for docker-compose binary
type ComposeStackManager struct { type ComposeStackManager struct {
deployer libstack.Deployer deployer libstack.Deployer
proxyManager *proxy.Manager proxyManager *proxy.Manager
dataStore dataservices.DataStore
} }
// NewComposeStackManager returns a docker-compose wrapper if corresponding binary present, otherwise nil // NewComposeStackManager returns a Compose stack manager
func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager) (*ComposeStackManager, error) { func NewComposeStackManager(deployer libstack.Deployer, proxyManager *proxy.Manager, dataStore dataservices.DataStore) *ComposeStackManager {
return &ComposeStackManager{ return &ComposeStackManager{
deployer: deployer, deployer: deployer,
proxyManager: proxyManager, proxyManager: proxyManager,
}, nil dataStore: dataStore,
}
} }
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax // ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
@@ -60,6 +65,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
EnvFilePath: envFilePath, EnvFilePath: envFilePath,
Host: url, Host: url,
ProjectName: stack.Name, ProjectName: stack.Name,
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
}, },
ForceRecreate: options.ForceRecreate, ForceRecreate: options.ForceRecreate,
AbortOnContainerExit: options.AbortOnContainerExit, AbortOnContainerExit: options.AbortOnContainerExit,
@@ -90,6 +96,7 @@ func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.St
EnvFilePath: envFilePath, EnvFilePath: envFilePath,
Host: url, Host: url,
ProjectName: stack.Name, ProjectName: stack.Name,
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
}, },
Remove: options.Remove, Remove: options.Remove,
Args: options.Args, Args: options.Args,
@@ -103,14 +110,15 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
url, proxy, err := manager.fetchEndpointProxy(endpoint) url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil { if err != nil {
return err return err
} } else if proxy != nil {
if proxy != nil {
defer proxy.Close() defer proxy.Close()
} }
err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.Options{ err = manager.deployer.Remove(ctx, stack.Name, nil, libstack.RemoveOptions{
WorkingDir: "", Options: libstack.Options{
Host: url, WorkingDir: "",
Host: url,
},
}) })
return errors.Wrap(err, "failed to remove a stack") return errors.Wrap(err, "failed to remove a stack")
@@ -118,12 +126,11 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
// Pull an image associated with a service defined in a docker-compose.yml or docker-stack.yml file, // Pull an image associated with a service defined in a docker-compose.yml or docker-stack.yml file,
// but does not start containers based on those images. // but does not start containers based on those images.
func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error { func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, options portainer.ComposeOptions) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint) url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil { if err != nil {
return err return err
} } else if proxy != nil {
if proxy != nil {
defer proxy.Close() defer proxy.Close()
} }
@@ -138,6 +145,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
EnvFilePath: envFilePath, EnvFilePath: envFilePath,
Host: url, Host: url,
ProjectName: stack.Name, ProjectName: stack.Name,
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
}) })
return errors.Wrap(err, "failed to pull images of the stack") return errors.Wrap(err, "failed to pull images of the stack")
} }
@@ -176,16 +184,16 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
// Copy from default .env file // Copy from default .env file
defaultEnvPath := path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint), ".env") defaultEnvPath := path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint), ".env")
if err = copyDefaultEnvFile(envfile, defaultEnvPath); err != nil { if err := copyDefaultEnvFile(envfile, defaultEnvPath); err != nil {
return "", err return "", err
} }
// Copy from stack env vars // Copy from stack env vars
if err = copyConfigEnvVars(envfile, stack.Env); err != nil { if err := copyConfigEnvVars(envfile, stack.Env); err != nil {
return "", err return "", err
} }
return "stack.env", nil return envFilePath, nil
} }
// copyDefaultEnvFile copies the default .env file if it exists to the provided writer // copyDefaultEnvFile copies the default .env file if it exists to the provided writer
@@ -217,3 +225,49 @@ func copyConfigEnvVars(w io.Writer, envs []portainer.Pair) error {
} }
return nil return nil
} }
func portainerRegistriesToAuthConfigs(tx dataservices.DataStoreTx, registries []portainer.Registry) []types.AuthConfig {
var authConfigs []types.AuthConfig
for _, r := range registries {
ac := types.AuthConfig{
Username: r.Username,
Password: r.Password,
ServerAddress: r.URL,
}
if r.Authentication {
var err error
ac.Username, ac.Password, err = getEffectiveRegUsernamePassword(tx, &r)
if err != nil {
continue
}
}
authConfigs = append(authConfigs, ac)
}
return authConfigs
}
func getEffectiveRegUsernamePassword(tx dataservices.DataStoreTx, registry *portainer.Registry) (string, string, error) {
if err := registryutils.EnsureRegTokenValid(tx, registry); err != nil {
log.Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to validate registry token. Skip logging with this registry.")
return "", "", err
}
username, password, err := registryutils.GetRegEffectiveCredential(registry)
if err != nil {
log.Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to get effective credential. Skip logging with this registry.")
}
return username, password, err
}

View File

@@ -2,7 +2,6 @@ package exec
import ( import (
"context" "context"
"fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -43,25 +42,17 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
} }
func Test_UpAndDown(t *testing.T) { func Test_UpAndDown(t *testing.T) {
testhelpers.IntegrationTest(t) testhelpers.IntegrationTest(t)
stack, endpoint := setup(t) stack, endpoint := setup(t)
deployer, err := compose.NewComposeDeployer("", "") deployer := compose.NewComposeDeployer()
if err != nil {
t.Fatal(err)
}
w, err := NewComposeStackManager(deployer, nil) w := NewComposeStackManager(deployer, nil, nil)
if err != nil {
t.Fatalf("Failed creating manager: %s", err)
}
ctx := context.TODO() ctx := context.TODO()
err = w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{}) if err := w.Up(ctx, stack, endpoint, portainer.ComposeUpOptions{}); err != nil {
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err) t.Fatalf("Error calling docker-compose up: %s", err)
} }
@@ -69,8 +60,7 @@ func Test_UpAndDown(t *testing.T) {
t.Fatal("container should exist") t.Fatal("container should exist")
} }
err = w.Down(ctx, stack, endpoint) if err := w.Down(ctx, stack, endpoint); err != nil {
if err != nil {
t.Fatalf("Error calling docker-compose down: %s", err) t.Fatalf("Error calling docker-compose down: %s", err)
} }
@@ -80,7 +70,7 @@ func Test_UpAndDown(t *testing.T) {
} }
func containerExists(containerName string) bool { func containerExists(containerName string) bool {
cmd := exec.Command("docker", "ps", "-a", "-f", fmt.Sprintf("name=%s", containerName)) cmd := exec.Command("docker", "ps", "-a", "-f", "name="+containerName)
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"io" "io"
"os" "os"
"path" "path"
"path/filepath"
"testing" "testing"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
@@ -53,7 +54,7 @@ func Test_createEnvFile(t *testing.T) {
result, _ := createEnvFile(tt.stack) result, _ := createEnvFile(tt.stack)
if tt.expected != "" { if tt.expected != "" {
assert.Equal(t, "stack.env", result) assert.Equal(t, filepath.Join(tt.stack.ProjectPath, "stack.env"), result)
f, _ := os.Open(path.Join(dir, "stack.env")) f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := io.ReadAll(f) content, _ := io.ReadAll(f)
@@ -77,7 +78,7 @@ func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
}, },
} }
result, err := createEnvFile(stack) result, err := createEnvFile(stack)
assert.Equal(t, "stack.env", result) assert.Equal(t, filepath.Join(stack.ProjectPath, "stack.env"), result)
assert.NoError(t, err) assert.NoError(t, err)
assert.FileExists(t, path.Join(dir, "stack.env")) assert.FileExists(t, path.Join(dir, "stack.env"))
f, _ := os.Open(path.Join(dir, "stack.env")) f, _ := os.Open(path.Join(dir, "stack.env"))

View File

@@ -4,10 +4,12 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
) )
type kubernetesMockDeployer struct{} type kubernetesMockDeployer struct {
portainer.KubernetesDeployer
}
// NewKubernetesDeployer creates a mock kubernetes deployer // NewKubernetesDeployer creates a mock kubernetes deployer
func NewKubernetesDeployer() portainer.KubernetesDeployer { func NewKubernetesDeployer() *kubernetesMockDeployer {
return &kubernetesMockDeployer{} return &kubernetesMockDeployer{}
} }
@@ -18,3 +20,7 @@ func (deployer *kubernetesMockDeployer) Deploy(userID portainer.UserID, endpoint
func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) { func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
return "", nil return "", nil
} }
func (deployer *kubernetesMockDeployer) Restart(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
return "", nil
}

View File

@@ -1,13 +1,8 @@
package exec package exec
import ( import (
"bytes" "context"
"fmt" "fmt"
"os"
"os/exec"
"path"
"runtime"
"strings"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
@@ -15,13 +10,17 @@ import (
"github.com/portainer/portainer/api/http/proxy/factory" "github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/pkg/libkubectl"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const (
defaultServerURL = "https://kubernetes.default.svc"
)
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment(endpoint). // KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment(endpoint).
type KubernetesDeployer struct { type KubernetesDeployer struct {
binaryPath string
dataStore dataservices.DataStore dataStore dataservices.DataStore
reverseTunnelService portainer.ReverseTunnelService reverseTunnelService portainer.ReverseTunnelService
signatureService portainer.DigitalSignatureService signatureService portainer.DigitalSignatureService
@@ -31,9 +30,8 @@ type KubernetesDeployer struct {
} }
// NewKubernetesDeployer initializes a new KubernetesDeployer service. // NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, binaryPath string) *KubernetesDeployer { func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore dataservices.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager) *KubernetesDeployer {
return &KubernetesDeployer{ return &KubernetesDeployer{
binaryPath: binaryPath,
dataStore: datastore, dataStore: datastore,
reverseTunnelService: reverseTunnelService, reverseTunnelService: reverseTunnelService,
signatureService: signatureService, signatureService: signatureService,
@@ -71,70 +69,63 @@ func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *
} }
if token == "" { if token == "" {
return "", fmt.Errorf("can not get a valid user service account token") return "", errors.New("can not get a valid user service account token")
} }
return token, nil return token, nil
} }
// Deploy upserts Kubernetes resources defined in manifest(s) // Deploy upserts Kubernetes resources defined in manifest(s)
func (deployer *KubernetesDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) { func (deployer *KubernetesDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, resources []string, namespace string) (string, error) {
return deployer.command("apply", userID, endpoint, manifestFiles, namespace) return deployer.command("apply", userID, endpoint, resources, namespace)
} }
// Remove deletes Kubernetes resources defined in manifest(s) // Remove deletes Kubernetes resources defined in manifest(s)
func (deployer *KubernetesDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) { func (deployer *KubernetesDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, resources []string, namespace string) (string, error) {
return deployer.command("delete", userID, endpoint, manifestFiles, namespace) return deployer.command("delete", userID, endpoint, resources, namespace)
} }
func (deployer *KubernetesDeployer) command(operation string, userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) { func (deployer *KubernetesDeployer) command(operation string, userID portainer.UserID, endpoint *portainer.Endpoint, resources []string, namespace string) (string, error) {
token, err := deployer.getToken(userID, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment) token, err := deployer.getToken(userID, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment)
if err != nil { if err != nil {
return "", errors.Wrap(err, "failed generating a user token") return "", errors.Wrap(err, "failed generating a user token")
} }
command := path.Join(deployer.binaryPath, "kubectl") serverURL := defaultServerURL
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := []string{"--token", token}
if namespace != "" {
args = append(args, "--namespace", namespace)
}
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment { if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
url, proxy, err := deployer.getAgentURL(endpoint) url, proxy, err := deployer.getAgentURL(endpoint)
if err != nil { if err != nil {
return "", errors.WithMessage(err, "failed generating endpoint URL") return "", errors.WithMessage(err, "failed generating endpoint URL")
} }
defer proxy.Close() defer proxy.Close()
args = append(args, "--server", url)
args = append(args, "--insecure-skip-tls-verify") serverURL = url
} }
if operation == "delete" { client, err := libkubectl.NewClient(&libkubectl.ClientAccess{
args = append(args, "--ignore-not-found=true") Token: token,
} ServerUrl: serverURL,
}, namespace, "", true)
args = append(args, operation)
for _, path := range manifestFiles {
args = append(args, "-f", strings.TrimSpace(path))
}
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "POD_NAMESPACE=default")
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil { if err != nil {
return "", errors.Wrapf(err, "failed to execute kubectl command: %q", stderr.String()) return "", errors.Wrap(err, "failed to create kubectl client")
} }
return string(output), nil operations := map[string]func(context.Context, []string) (string, error){
"apply": client.Apply,
"delete": client.Delete,
}
operationFunc, ok := operations[operation]
if !ok {
return "", errors.Errorf("unsupported operation: %s", operation)
}
output, err := operationFunc(context.Background(), resources)
if err != nil {
return "", errors.Wrapf(err, "failed to execute kubectl %s command", operation)
}
return output, nil
} }
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) { func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {

View File

@@ -0,0 +1,173 @@
package exec
import (
"context"
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type mockKubectlClient struct {
applyFunc func(ctx context.Context, files []string) error
deleteFunc func(ctx context.Context, files []string) error
rolloutRestartFunc func(ctx context.Context, resources []string) error
}
func (m *mockKubectlClient) Apply(ctx context.Context, files []string) error {
if m.applyFunc != nil {
return m.applyFunc(ctx, files)
}
return nil
}
func (m *mockKubectlClient) Delete(ctx context.Context, files []string) error {
if m.deleteFunc != nil {
return m.deleteFunc(ctx, files)
}
return nil
}
func (m *mockKubectlClient) RolloutRestart(ctx context.Context, resources []string) error {
if m.rolloutRestartFunc != nil {
return m.rolloutRestartFunc(ctx, resources)
}
return nil
}
func testExecuteKubectlOperation(client *mockKubectlClient, operation string, manifestFiles []string) error {
operations := map[string]func(context.Context, []string) error{
"apply": client.Apply,
"delete": client.Delete,
"rollout-restart": client.RolloutRestart,
}
operationFunc, ok := operations[operation]
if !ok {
return fmt.Errorf("unsupported operation: %s", operation)
}
if err := operationFunc(context.Background(), manifestFiles); err != nil {
return fmt.Errorf("failed to execute kubectl %s command: %w", operation, err)
}
return nil
}
func TestExecuteKubectlOperation_Apply_Success(t *testing.T) {
called := false
mockClient := &mockKubectlClient{
applyFunc: func(ctx context.Context, files []string) error {
called = true
assert.Equal(t, []string{"manifest1.yaml", "manifest2.yaml"}, files)
return nil
},
}
manifests := []string{"manifest1.yaml", "manifest2.yaml"}
err := testExecuteKubectlOperation(mockClient, "apply", manifests)
assert.NoError(t, err)
assert.True(t, called)
}
func TestExecuteKubectlOperation_Apply_Error(t *testing.T) {
expectedErr := errors.New("kubectl apply failed")
called := false
mockClient := &mockKubectlClient{
applyFunc: func(ctx context.Context, files []string) error {
called = true
assert.Equal(t, []string{"error.yaml"}, files)
return expectedErr
},
}
manifests := []string{"error.yaml"}
err := testExecuteKubectlOperation(mockClient, "apply", manifests)
assert.Error(t, err)
assert.Contains(t, err.Error(), expectedErr.Error())
assert.True(t, called)
}
func TestExecuteKubectlOperation_Delete_Success(t *testing.T) {
called := false
mockClient := &mockKubectlClient{
deleteFunc: func(ctx context.Context, files []string) error {
called = true
assert.Equal(t, []string{"manifest1.yaml"}, files)
return nil
},
}
manifests := []string{"manifest1.yaml"}
err := testExecuteKubectlOperation(mockClient, "delete", manifests)
assert.NoError(t, err)
assert.True(t, called)
}
func TestExecuteKubectlOperation_Delete_Error(t *testing.T) {
expectedErr := errors.New("kubectl delete failed")
called := false
mockClient := &mockKubectlClient{
deleteFunc: func(ctx context.Context, files []string) error {
called = true
assert.Equal(t, []string{"error.yaml"}, files)
return expectedErr
},
}
manifests := []string{"error.yaml"}
err := testExecuteKubectlOperation(mockClient, "delete", manifests)
assert.Error(t, err)
assert.Contains(t, err.Error(), expectedErr.Error())
assert.True(t, called)
}
func TestExecuteKubectlOperation_RolloutRestart_Success(t *testing.T) {
called := false
mockClient := &mockKubectlClient{
rolloutRestartFunc: func(ctx context.Context, resources []string) error {
called = true
assert.Equal(t, []string{"deployment/nginx"}, resources)
return nil
},
}
resources := []string{"deployment/nginx"}
err := testExecuteKubectlOperation(mockClient, "rollout-restart", resources)
assert.NoError(t, err)
assert.True(t, called)
}
func TestExecuteKubectlOperation_RolloutRestart_Error(t *testing.T) {
expectedErr := errors.New("kubectl rollout restart failed")
called := false
mockClient := &mockKubectlClient{
rolloutRestartFunc: func(ctx context.Context, resources []string) error {
called = true
assert.Equal(t, []string{"deployment/error"}, resources)
return expectedErr
},
}
resources := []string{"deployment/error"}
err := testExecuteKubectlOperation(mockClient, "rollout-restart", resources)
assert.Error(t, err)
assert.Contains(t, err.Error(), expectedErr.Error())
assert.True(t, called)
}
func TestExecuteKubectlOperation_UnsupportedOperation(t *testing.T) {
mockClient := &mockKubectlClient{}
err := testExecuteKubectlOperation(mockClient, "unsupported", []string{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unsupported operation")
}

View File

@@ -11,7 +11,6 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices" "github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/stacks/stackutils" "github.com/portainer/portainer/api/stacks/stackutils"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@@ -46,8 +45,7 @@ func NewSwarmStackManager(
dataStore: datastore, dataStore: datastore,
} }
err := manager.updateDockerCLIConfiguration(manager.configPath) if err := manager.updateDockerCLIConfiguration(manager.configPath); err != nil {
if err != nil {
return nil, err return nil, err
} }
@@ -63,33 +61,14 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
for _, registry := range registries { for _, registry := range registries {
if registry.Authentication { if registry.Authentication {
err = registryutils.EnsureRegTokenValid(manager.dataStore, &registry) username, password, err := getEffectiveRegUsernamePassword(manager.dataStore, &registry)
if err != nil { if err != nil {
log.
Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to validate registry token. Skip logging with this registry.")
continue
}
username, password, err := registryutils.GetRegEffectiveCredential(&registry)
if err != nil {
log.
Warn().
Err(err).
Str("RegistryName", registry.Name).
Msg("Failed to get effective credential. Skip logging with this registry.")
continue continue
} }
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL) registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
err = runCommandAndCaptureStdErr(command, registryArgs, nil, "") if err := runCommandAndCaptureStdErr(command, registryArgs, nil, ""); err != nil {
if err != nil { log.Warn().
log.
Warn().
Err(err). Err(err).
Str("RegistryName", registry.Name). Str("RegistryName", registry.Name).
Msg("Failed to login.") Msg("Failed to login.")
@@ -148,13 +127,14 @@ func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *porta
return err return err
} }
args = append(args, "stack", "rm", stack.Name) args = append(args, "stack", "rm", "--detach=false", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil, "") return runCommandAndCaptureStdErr(command, args, nil, "")
} }
func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error { func runCommandAndCaptureStdErr(command string, args []string, env []string, workingDir string) error {
var stderr bytes.Buffer var stderr bytes.Buffer
cmd := exec.Command(command, args...) cmd := exec.Command(command, args...)
cmd.Stderr = &stderr cmd.Stderr = &stderr
@@ -167,8 +147,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
cmd.Env = append(cmd.Env, env...) cmd.Env = append(cmd.Env, env...)
} }
err := cmd.Run() if err := cmd.Run(); err != nil {
if err != nil {
return errors.New(stderr.String()) return errors.New(stderr.String())
} }
@@ -192,6 +171,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
endpointURL = "tcp://" + tunnelAddr endpointURL = "tcp://" + tunnelAddr
} }
@@ -216,9 +196,10 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error { func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
configFilePath := path.Join(configPath, "config.json") configFilePath := path.Join(configPath, "config.json")
config, err := manager.retrieveConfigurationFromDisk(configFilePath) config, err := manager.retrieveConfigurationFromDisk(configFilePath)
if err != nil { if err != nil {
return err log.Warn().Err(err).Msg("unable to retrieve the Swarm configuration from disk, proceeding without it")
} }
signature, err := manager.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) signature, err := manager.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
@@ -246,8 +227,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
return make(map[string]any), nil return make(map[string]any), nil
} }
err = json.Unmarshal(raw, &config) if err := json.Unmarshal(raw, &config); err != nil {
if err != nil {
return nil, err return nil, err
} }

View File

@@ -68,7 +68,7 @@ func copyFile(src, dst string) error {
defer from.Close() defer from.Close()
// has to include 'execute' bit, otherwise fails. MkdirAll follows `mkdir -m` restrictions // has to include 'execute' bit, otherwise fails. MkdirAll follows `mkdir -m` restrictions
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil { if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err return err
} }
to, err := os.Create(dst) to, err := os.Create(dst)

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
@@ -357,7 +358,7 @@ func (service *Service) RollbackStackFile(stackIdentifier, fileName string) erro
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier) stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
composeFilePath := JoinPaths(stackStorePath, fileName) composeFilePath := JoinPaths(stackStorePath, fileName)
path := service.wrapFileStore(composeFilePath) path := service.wrapFileStore(composeFilePath)
backupPath := fmt.Sprintf("%s.bak", path) backupPath := path + ".bak"
exists, err := service.FileExists(backupPath) exists, err := service.FileExists(backupPath)
if err != nil { if err != nil {
@@ -381,12 +382,12 @@ func (service *Service) RollbackStackFile(stackIdentifier, fileName string) erro
func (service *Service) RollbackStackFileByVersion(stackIdentifier string, version int, fileName string) error { func (service *Service) RollbackStackFileByVersion(stackIdentifier string, version int, fileName string) error {
versionStr := "" versionStr := ""
if version != 0 { if version != 0 {
versionStr = fmt.Sprintf("v%d", version) versionStr = "v" + strconv.Itoa(version)
} }
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier, versionStr) stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier, versionStr)
composeFilePath := JoinPaths(stackStorePath, fileName) composeFilePath := JoinPaths(stackStorePath, fileName)
path := service.wrapFileStore(composeFilePath) path := service.wrapFileStore(composeFilePath)
backupPath := fmt.Sprintf("%s.bak", path) backupPath := path + ".bak"
exists, err := service.FileExists(backupPath) exists, err := service.FileExists(backupPath)
if err != nil { if err != nil {
@@ -671,7 +672,7 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
// createBackupFileInStore makes a copy in the file store. // createBackupFileInStore makes a copy in the file store.
func (service *Service) createBackupFileInStore(filePath string) error { func (service *Service) createBackupFileInStore(filePath string) error {
path := service.wrapFileStore(filePath) path := service.wrapFileStore(filePath)
backupPath := fmt.Sprintf("%s.bak", path) backupPath := path + ".bak"
return service.Copy(path, backupPath, true) return service.Copy(path, backupPath, true)
} }
@@ -679,7 +680,7 @@ func (service *Service) createBackupFileInStore(filePath string) error {
// removeBackupFileInStore removes the copy in the file store. // removeBackupFileInStore removes the copy in the file store.
func (service *Service) removeBackupFileInStore(filePath string) error { func (service *Service) removeBackupFileInStore(filePath string) error {
path := service.wrapFileStore(filePath) path := service.wrapFileStore(filePath)
backupPath := fmt.Sprintf("%s.bak", path) backupPath := path + ".bak"
exists, err := service.FileExists(backupPath) exists, err := service.FileExists(backupPath)
if err != nil { if err != nil {
@@ -799,7 +800,7 @@ func (service *Service) StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID strin
return err return err
} }
filePath := JoinPaths(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID)) filePath := JoinPaths(edgeJobStorePath, "logs_"+taskID)
r := bytes.NewReader(data) r := bytes.NewReader(data)
return service.createFileInStore(filePath, r) return service.createFileInStore(filePath, r)
} }
@@ -840,11 +841,11 @@ func (service *Service) GetDefaultSSLCertsPath() (string, string) {
} }
func defaultMTLSCertPathUnderFileStore() (string, string, string) { func defaultMTLSCertPathUnderFileStore() (string, string, string) {
certPath := JoinPaths(SSLCertPath, MTLSCertFilename)
caCertPath := JoinPaths(SSLCertPath, MTLSCACertFilename) caCertPath := JoinPaths(SSLCertPath, MTLSCACertFilename)
certPath := JoinPaths(SSLCertPath, MTLSCertFilename)
keyPath := JoinPaths(SSLCertPath, MTLSKeyFilename) keyPath := JoinPaths(SSLCertPath, MTLSKeyFilename)
return certPath, caCertPath, keyPath return caCertPath, certPath, keyPath
} }
// GetDefaultChiselPrivateKeyPath returns the chisle private key path // GetDefaultChiselPrivateKeyPath returns the chisle private key path
@@ -990,7 +991,7 @@ func MoveDirectory(originalPath, newPath string, overwriteTargetPath bool) error
if alreadyExists { if alreadyExists {
if !overwriteTargetPath { if !overwriteTargetPath {
return fmt.Errorf("Target path already exists") return errors.New("Target path already exists")
} }
if err = os.RemoveAll(newPath); err != nil { if err = os.RemoveAll(newPath); err != nil {
@@ -1013,26 +1014,45 @@ func CreateFile(path string, r io.Reader) error {
return err return err
} }
func (service *Service) StoreMTLSCertificates(cert, caCert, key []byte) (string, string, string, error) { func (service *Service) StoreMTLSCertificates(caCert, cert, key []byte) (string, string, string, error) {
certPath, caCertPath, keyPath := defaultMTLSCertPathUnderFileStore() caCertPath, certPath, keyPath := defaultMTLSCertPathUnderFileStore()
r := bytes.NewReader(cert) r := bytes.NewReader(caCert)
err := service.createFileInStore(certPath, r) if err := service.createFileInStore(caCertPath, r); err != nil {
if err != nil {
return "", "", "", err return "", "", "", err
} }
r = bytes.NewReader(caCert) r = bytes.NewReader(cert)
err = service.createFileInStore(caCertPath, r) if err := service.createFileInStore(certPath, r); err != nil {
if err != nil {
return "", "", "", err return "", "", "", err
} }
r = bytes.NewReader(key) r = bytes.NewReader(key)
err = service.createFileInStore(keyPath, r) if err := service.createFileInStore(keyPath, r); err != nil {
if err != nil {
return "", "", "", err return "", "", "", err
} }
return service.wrapFileStore(certPath), service.wrapFileStore(caCertPath), service.wrapFileStore(keyPath), nil return service.wrapFileStore(caCertPath), service.wrapFileStore(certPath), service.wrapFileStore(keyPath), nil
}
func (service *Service) GetMTLSCertificates() (string, string, string, error) {
caCertPath, certPath, keyPath := defaultMTLSCertPathUnderFileStore()
caCertPath = service.wrapFileStore(caCertPath)
certPath = service.wrapFileStore(certPath)
keyPath = service.wrapFileStore(keyPath)
paths := [...]string{caCertPath, certPath, keyPath}
for _, path := range paths {
exists, err := service.FileExists(path)
if err != nil {
return "", "", "", err
}
if !exists {
return "", "", "", fmt.Errorf("file %s does not exist", path)
}
}
return caCertPath, certPath, keyPath, nil
} }

View File

@@ -51,7 +51,7 @@ func FilterDirForEntryFile(dirEntries []DirEntry, entryFile string) []DirEntry {
// FilterDirForCompatibility returns the content of the entry file if agent version is less than 2.19.0 // FilterDirForCompatibility returns the content of the entry file if agent version is less than 2.19.0
func FilterDirForCompatibility(dirEntries []DirEntry, entryFilePath, agentVersion string) (string, error) { func FilterDirForCompatibility(dirEntries []DirEntry, entryFilePath, agentVersion string) (string, error) {
if semver.Compare(fmt.Sprintf("v%s", agentVersion), "v2.19.0") == -1 { if semver.Compare("v"+agentVersion, "v2.19.0") == -1 {
for _, dirEntry := range dirEntries { for _, dirEntry := range dirEntries {
if dirEntry.IsFile { if dirEntry.IsFile {
if dirEntry.Name == entryFilePath { if dirEntry.Name == entryFilePath {

View File

@@ -15,15 +15,19 @@ type MultiFilterArgs []struct {
} }
// MultiFilterDirForPerDevConfigs filers the given dirEntries with multiple filter args, returns the merged entries for the given device // MultiFilterDirForPerDevConfigs filers the given dirEntries with multiple filter args, returns the merged entries for the given device
func MultiFilterDirForPerDevConfigs(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs) []DirEntry { func MultiFilterDirForPerDevConfigs(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs) ([]DirEntry, []string) {
var filteredDirEntries []DirEntry var filteredDirEntries []DirEntry
var envFiles []string
for _, multiFilterArg := range multiFilterArgs { for _, multiFilterArg := range multiFilterArgs {
tmp := FilterDirForPerDevConfigs(dirEntries, multiFilterArg.FilterKey, configPath, multiFilterArg.FilterType) tmp, efs := FilterDirForPerDevConfigs(dirEntries, multiFilterArg.FilterKey, configPath, multiFilterArg.FilterType)
filteredDirEntries = append(filteredDirEntries, tmp...) filteredDirEntries = append(filteredDirEntries, tmp...)
envFiles = append(envFiles, efs...)
} }
return deduplicate(filteredDirEntries) return deduplicate(filteredDirEntries), envFiles
} }
func deduplicate(dirEntries []DirEntry) []DirEntry { func deduplicate(dirEntries []DirEntry) []DirEntry {
@@ -32,8 +36,7 @@ func deduplicate(dirEntries []DirEntry) []DirEntry {
marks := make(map[string]struct{}) marks := make(map[string]struct{})
for _, dirEntry := range dirEntries { for _, dirEntry := range dirEntries {
_, ok := marks[dirEntry.Name] if _, ok := marks[dirEntry.Name]; !ok {
if !ok {
marks[dirEntry.Name] = struct{}{} marks[dirEntry.Name] = struct{}{}
deduplicatedDirEntries = append(deduplicatedDirEntries, dirEntry) deduplicatedDirEntries = append(deduplicatedDirEntries, dirEntry)
} }
@@ -44,34 +47,33 @@ func deduplicate(dirEntries []DirEntry) []DirEntry {
// FilterDirForPerDevConfigs filers the given dirEntries, returns entries for the given device // FilterDirForPerDevConfigs filers the given dirEntries, returns entries for the given device
// For given configPath A/B/C, return entries: // For given configPath A/B/C, return entries:
// 1. all entries outside of dir A // 1. all entries outside of dir A/B/C
// 2. dir entries A, A/B, A/B/C // 2. For filterType file:
// 3. For filterType file:
// file entries: A/B/C/<deviceName> and A/B/C/<deviceName>.* // file entries: A/B/C/<deviceName> and A/B/C/<deviceName>.*
// 4. For filterType dir: // 3. For filterType dir:
// dir entry: A/B/C/<deviceName> // dir entry: A/B/C/<deviceName>
// all entries: A/B/C/<deviceName>/* // all entries: A/B/C/<deviceName>/*
func FilterDirForPerDevConfigs(dirEntries []DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) []DirEntry { func FilterDirForPerDevConfigs(dirEntries []DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) ([]DirEntry, []string) {
var filteredDirEntries []DirEntry var filteredDirEntries []DirEntry
var envFiles []string
for _, dirEntry := range dirEntries { for _, dirEntry := range dirEntries {
if shouldIncludeEntry(dirEntry, deviceName, configPath, filterType) { if shouldIncludeEntry(dirEntry, deviceName, configPath, filterType) {
filteredDirEntries = append(filteredDirEntries, dirEntry) filteredDirEntries = append(filteredDirEntries, dirEntry)
if shouldParseEnvVars(dirEntry, deviceName, configPath, filterType) {
envFiles = append(envFiles, dirEntry.Name)
}
} }
} }
return filteredDirEntries return filteredDirEntries, envFiles
} }
func shouldIncludeEntry(dirEntry DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) bool { func shouldIncludeEntry(dirEntry DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) bool {
// Include all entries outside of dir A // Include all entries outside of dir A
if !isInConfigRootDir(dirEntry, configPath) { if !isInConfigDir(dirEntry, configPath) {
return true
}
// Include dir entries A, A/B, A/B/C
if isParentDir(dirEntry, configPath) {
return true return true
} }
@@ -90,21 +92,9 @@ func shouldIncludeEntry(dirEntry DirEntry, deviceName, configPath string, filter
return false return false
} }
func isInConfigRootDir(dirEntry DirEntry, configPath string) bool { func isInConfigDir(dirEntry DirEntry, configPath string) bool {
// get the first element of the configPath // return true if entry name starts with "A/B"
rootDir := strings.Split(configPath, string(os.PathSeparator))[0] return strings.HasPrefix(dirEntry.Name, appendTailSeparator(configPath))
// return true if entry name starts with "A/"
return strings.HasPrefix(dirEntry.Name, appendTailSeparator(rootDir))
}
func isParentDir(dirEntry DirEntry, configPath string) bool {
if dirEntry.IsFile {
return false
}
// return true for dir entries A, A/B, A/B/C
return strings.HasPrefix(appendTailSeparator(configPath), appendTailSeparator(dirEntry.Name))
} }
func shouldIncludeFile(dirEntry DirEntry, deviceName, configPath string) bool { func shouldIncludeFile(dirEntry DirEntry, deviceName, configPath string) bool {
@@ -116,7 +106,7 @@ func shouldIncludeFile(dirEntry DirEntry, deviceName, configPath string) bool {
filterEqual := filepath.Join(configPath, deviceName) filterEqual := filepath.Join(configPath, deviceName)
// example: A/B/C/<deviceName>/ // example: A/B/C/<deviceName>/
filterPrefix := fmt.Sprintf("%s.", filterEqual) filterPrefix := filterEqual + "."
// include file entries: A/B/C/<deviceName> or A/B/C/<deviceName>.* // include file entries: A/B/C/<deviceName> or A/B/C/<deviceName>.*
return dirEntry.Name == filterEqual || strings.HasPrefix(dirEntry.Name, filterPrefix) return dirEntry.Name == filterEqual || strings.HasPrefix(dirEntry.Name, filterPrefix)
@@ -138,6 +128,15 @@ func shouldIncludeDir(dirEntry DirEntry, deviceName, configPath string) bool {
return strings.HasPrefix(dirEntry.Name, filterPrefix) return strings.HasPrefix(dirEntry.Name, filterPrefix)
} }
func shouldParseEnvVars(dirEntry DirEntry, deviceName, configPath string, filterType portainer.PerDevConfigsFilterType) bool {
if !dirEntry.IsFile {
return false
}
return isInConfigDir(dirEntry, configPath) &&
filepath.Base(dirEntry.Name) == deviceName+".env"
}
func appendTailSeparator(path string) string { func appendTailSeparator(path string) string {
return fmt.Sprintf("%s%c", path, os.PathSeparator) return fmt.Sprintf("%s%c", path, os.PathSeparator)
} }

View File

@@ -4,14 +4,17 @@ import (
"testing" "testing"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestMultiFilterDirForPerDevConfigs(t *testing.T) { func TestMultiFilterDirForPerDevConfigs(t *testing.T) {
type args struct { f := func(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs, wantDirEntries []DirEntry) {
dirEntries []DirEntry t.Helper()
configPath string
multiFilterArgs MultiFilterArgs dirEntries, _ = MultiFilterDirForPerDevConfigs(dirEntries, configPath, multiFilterArgs)
require.Equal(t, wantDirEntries, dirEntries)
} }
baseDirEntries := []DirEntry{ baseDirEntries := []DirEntry{
@@ -26,67 +29,94 @@ func TestMultiFilterDirForPerDevConfigs(t *testing.T) {
{"configs/folder2/config2", "", true, 420}, {"configs/folder2/config2", "", true, 420},
} }
tests := []struct { // Filter file1
name string f(
args args baseDirEntries,
want []DirEntry "configs",
}{ MultiFilterArgs{{"file1", portainer.PerDevConfigsTypeFile}},
{ []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3]},
name: "filter file1", )
args: args{
baseDirEntries, // Filter folder1
"configs", f(
MultiFilterArgs{{"file1", portainer.PerDevConfigsTypeFile}}, baseDirEntries,
}, "configs",
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3]}, MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
)
// Filter file1 and folder1
f(
baseDirEntries,
"configs",
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
)
// Filter file1 and file2
f(
baseDirEntries,
"configs",
MultiFilterArgs{
{"file1", portainer.PerDevConfigsTypeFile},
{"file2", portainer.PerDevConfigsTypeFile},
}, },
{ []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[4]},
name: "filter folder1", )
args: args{
baseDirEntries, // Filter folder1 and folder2
"configs", f(
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}}, baseDirEntries,
}, "configs",
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]}, MultiFilterArgs{
}, {"folder1", portainer.PerDevConfigsTypeDir},
{ {"folder2", portainer.PerDevConfigsTypeDir},
name: "filter file1 and folder1",
args: args{
baseDirEntries,
"configs",
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
},
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
},
{
name: "filter file1 and file2",
args: args{
baseDirEntries,
"configs",
MultiFilterArgs{
{"file1", portainer.PerDevConfigsTypeFile},
{"file2", portainer.PerDevConfigsTypeFile},
},
},
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[4]},
},
{
name: "filter folder1 and folder2",
args: args{
baseDirEntries,
"configs",
MultiFilterArgs{
{"folder1", portainer.PerDevConfigsTypeDir},
{"folder2", portainer.PerDevConfigsTypeDir},
},
},
want: []DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6], baseDirEntries[7], baseDirEntries[8]},
}, },
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6], baseDirEntries[7], baseDirEntries[8]},
)
}
func TestMultiFilterDirForPerDevConfigsEnvFiles(t *testing.T) {
f := func(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs, wantEnvFiles []string) {
t.Helper()
_, envFiles := MultiFilterDirForPerDevConfigs(dirEntries, configPath, multiFilterArgs)
require.Equal(t, wantEnvFiles, envFiles)
} }
for _, tt := range tests { baseDirEntries := []DirEntry{
t.Run(tt.name, func(t *testing.T) { {".env", "", true, 420},
assert.Equalf(t, tt.want, MultiFilterDirForPerDevConfigs(tt.args.dirEntries, tt.args.configPath, tt.args.multiFilterArgs), "MultiFilterDirForPerDevConfigs(%v, %v, %v)", tt.args.dirEntries, tt.args.configPath, tt.args.multiFilterArgs) {"docker-compose.yaml", "", true, 420},
}) {"configs", "", false, 420},
{"configs/edge-id/edge-id.env", "", true, 420},
} }
f(
baseDirEntries,
"configs",
MultiFilterArgs{{"edge-id", portainer.PerDevConfigsTypeDir}},
[]string{"configs/edge-id/edge-id.env"},
)
}
func TestIsInConfigDir(t *testing.T) {
f := func(dirEntry DirEntry, configPath string, expect bool) {
t.Helper()
actual := isInConfigDir(dirEntry, configPath)
assert.Equal(t, expect, actual)
}
f(DirEntry{Name: "edge-configs"}, "edge-configs", false)
f(DirEntry{Name: "edge-configs_backup"}, "edge-configs", false)
f(DirEntry{Name: "edge-configs/standalone-edge-agent-standard"}, "edge-configs", true)
f(DirEntry{Name: "parent/edge-configs/"}, "edge-configs", false)
f(DirEntry{Name: "edgestacktest"}, "edgestacktest/edge-configs", false)
f(DirEntry{Name: "edgestacktest/edgeconfigs-test.yaml"}, "edgestacktest/edge-configs", false)
f(DirEntry{Name: "edgestacktest/file1.conf"}, "edgestacktest/edge-configs", false)
f(DirEntry{Name: "edgeconfigs-test.yaml"}, "edgestacktest/edge-configs", false)
f(DirEntry{Name: "edgestacktest/edge-configs"}, "edgestacktest/edge-configs", false)
f(DirEntry{Name: "edgestacktest/edge-configs/standalone-edge-agent-async"}, "edgestacktest/edge-configs", true)
f(DirEntry{Name: "edgestacktest/edge-configs/abc.txt"}, "edgestacktest/edge-configs", true)
} }

View File

@@ -1,12 +1,11 @@
package git package git
import ( import (
"fmt"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types" gittypes "github.com/portainer/portainer/api/git/types"
"github.com/pkg/errors"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -25,32 +24,28 @@ type CloneOptions struct {
} }
func CloneWithBackup(gitService portainer.GitService, fileService portainer.FileService, options CloneOptions) (clean func(), err error) { func CloneWithBackup(gitService portainer.GitService, fileService portainer.FileService, options CloneOptions) (clean func(), err error) {
backupProjectPath := fmt.Sprintf("%s-old", options.ProjectPath) backupProjectPath := options.ProjectPath + "-old"
cleanUp := false cleanUp := false
cleanFn := func() { cleanFn := func() {
if !cleanUp { if !cleanUp {
return return
} }
err = fileService.RemoveDirectory(backupProjectPath) if err := fileService.RemoveDirectory(backupProjectPath); err != nil {
if err != nil {
log.Warn().Err(err).Msg("unable to remove git repository directory") log.Warn().Err(err).Msg("unable to remove git repository directory")
} }
} }
err = filesystem.MoveDirectory(options.ProjectPath, backupProjectPath, true) if err := filesystem.MoveDirectory(options.ProjectPath, backupProjectPath, true); err != nil {
if err != nil {
return cleanFn, errors.WithMessage(err, "Unable to move git repository directory") return cleanFn, errors.WithMessage(err, "Unable to move git repository directory")
} }
cleanUp = true cleanUp = true
err = gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify) if err := gitService.CloneRepository(options.ProjectPath, options.URL, options.ReferenceName, options.Username, options.Password, options.TLSSkipVerify); err != nil {
if err != nil {
cleanUp = false cleanUp = false
restoreError := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false) if err := filesystem.MoveDirectory(backupProjectPath, options.ProjectPath, false); err != nil {
if restoreError != nil { log.Warn().Err(err).Msg("failed restoring backup folder")
log.Warn().Err(restoreError).Msg("failed restoring backup folder")
} }
if errors.Is(err, gittypes.ErrAuthenticationFailure) { if errors.Is(err, gittypes.ErrAuthenticationFailure) {

View File

@@ -34,6 +34,7 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e
Depth: opt.depth, Depth: opt.depth,
InsecureSkipTLS: opt.tlsSkipVerify, InsecureSkipTLS: opt.tlsSkipVerify,
Auth: getAuth(opt.username, opt.password), Auth: getAuth(opt.username, opt.password),
Tags: git.NoTags,
} }
if opt.referenceName != "" { if opt.referenceName != "" {

View File

@@ -24,8 +24,7 @@ func setup(t *testing.T) string {
t.Fatal(errors.Wrap(err, "failed to open an archive")) t.Fatal(errors.Wrap(err, "failed to open an archive"))
} }
err = archive.ExtractTarGz(file, dir) if err := archive.ExtractTarGz(file, dir); err != nil {
if err != nil {
t.Fatal(errors.Wrapf(err, "failed to extract file from the archive to a folder %s", dir)) t.Fatal(errors.Wrapf(err, "failed to extract file from the archive to a folder %s", dir))
} }

View File

@@ -3,9 +3,9 @@ package update
import ( import (
"time" "time"
"github.com/asaskevich/govalidator"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors" httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/pkg/validate"
) )
func ValidateAutoUpdateSettings(autoUpdate *portainer.AutoUpdateSettings) error { func ValidateAutoUpdateSettings(autoUpdate *portainer.AutoUpdateSettings) error {
@@ -17,7 +17,7 @@ func ValidateAutoUpdateSettings(autoUpdate *portainer.AutoUpdateSettings) error
return httperrors.NewInvalidPayloadError("Webhook or Interval must be provided") return httperrors.NewInvalidPayloadError("Webhook or Interval must be provided")
} }
if autoUpdate.Webhook != "" && !govalidator.IsUUID(autoUpdate.Webhook) { if autoUpdate.Webhook != "" && !validate.IsUUID(autoUpdate.Webhook) {
return httperrors.NewInvalidPayloadError("invalid Webhook format") return httperrors.NewInvalidPayloadError("invalid Webhook format")
} }

View File

@@ -1,19 +1,17 @@
package git package git
import ( import (
"github.com/asaskevich/govalidator"
gittypes "github.com/portainer/portainer/api/git/types" gittypes "github.com/portainer/portainer/api/git/types"
httperrors "github.com/portainer/portainer/api/http/errors" httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/pkg/validate"
) )
func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error { func ValidateRepoConfig(repoConfig *gittypes.RepoConfig) error {
if len(repoConfig.URL) == 0 || !govalidator.IsURL(repoConfig.URL) { if len(repoConfig.URL) == 0 || !validate.IsURL(repoConfig.URL) {
return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format") return httperrors.NewInvalidPayloadError("Invalid repository URL. Must correspond to a valid URL format")
} }
return ValidateRepoAuthentication(repoConfig.Authentication) return ValidateRepoAuthentication(repoConfig.Authentication)
} }
func ValidateRepoAuthentication(auth *gittypes.GitAuthentication) error { func ValidateRepoAuthentication(auth *gittypes.GitAuthentication) error {

View File

@@ -123,7 +123,7 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
if err != nil { if err != nil {
return "", err return "", err
} }
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", configuration.MPSToken)) req.Header.Set("Authorization", "Bearer "+configuration.MPSToken)
response, err := service.httpsClient.Do(req) response, err := service.httpsClient.Do(req)
if err != nil { if err != nil {

View File

@@ -44,13 +44,13 @@ func (service *Service) executeDeviceAction(configuration portainer.OpenAMTConfi
} }
func parseAction(actionRaw string) (portainer.PowerState, error) { func parseAction(actionRaw string) (portainer.PowerState, error) {
switch strings.ToLower(actionRaw) { if strings.EqualFold(actionRaw, "power on") {
case "power on":
return powerOnState, nil return powerOnState, nil
case "power off": } else if strings.EqualFold(actionRaw, "power off") {
return powerOffState, nil return powerOffState, nil
case "restart": } else if strings.EqualFold(actionRaw, "restart") {
return restartState, nil return restartState, nil
} }
return 0, fmt.Errorf("unsupported device action %s", actionRaw) return 0, fmt.Errorf("unsupported device action %s", actionRaw)
} }

View File

@@ -97,7 +97,7 @@ func (service *Service) executeSaveRequest(method string, url string, token stri
return nil, err return nil, err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Authorization", "Bearer "+token)
response, err := service.httpsClient.Do(req) response, err := service.httpsClient.Do(req)
if err != nil { if err != nil {
@@ -128,7 +128,7 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Authorization", "Bearer "+token)
response, err := service.httpsClient.Do(req) response, err := service.httpsClient.Do(req)
if err != nil { if err != nil {

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