Compare commits

..

212 Commits

Author SHA1 Message Date
andres-portainer
da3856503f [WIP] Optimize buildEdgeStacks() 2022-05-11 19:38:55 -03:00
andres-portainer
0b6362a4bd Merge branch 'develop' into debug-api-endpoint 2022-05-10 18:00:25 -03:00
Dmitry Salakhov
45b300eaff fix(settings): allow empty edge url (#6907) 2022-05-10 15:51:12 -03:00
andres-portainer
ad7545f009 fix(tls): downgrade minimum version to TLS 1.2 to avoid proxy problems EE-3152 (#6909) 2022-05-10 15:33:53 -03:00
matias-portainer
5df30b9eb0 chore(edge): add unit tests to edgestatus inspect endpoint EE-3088 (#6905)
* chore(edge): add unit tests to edgestatus inspect endpoint EE-3088
2022-05-10 11:58:19 -03:00
Ali
2e0555dbca refactor(docker networks): migrate docker network detail view to react EE-2196 (#6700)
* Migrate network details to react
2022-05-10 09:01:15 +12:00
andres-portainer
8bbb6e5a6e Revert "Add caching to EdgeStack."
This reverts commit ea71ce44fa.
2022-05-08 16:41:11 -03:00
andres-portainer
ea71ce44fa Add caching to EdgeStack. 2022-05-06 21:45:06 -03:00
matias.spinarolli
a3f2b4b0af feat(ssl): use ECDSA instead of RSA to generate the self-signed certificates EE-3097 2022-05-06 19:35:52 -03:00
itsconquest
9650aa56c7 fix(extension): always restart the backend [EE-3093] (#6890) 2022-05-06 15:14:24 +12:00
itsconquest
0beb0d95c1 fix(extension): add missing labels [EE-3068] (#6879)
* fix(extension): add missing labels [EE-3068]

* fix(extension): add missing labels [EE-3068]
2022-05-06 14:19:26 +12:00
andres-portainer
f4c7896046 Add cache invalidation to avoid breaking data migration. 2022-05-05 19:40:40 -03:00
andres-portainer
b1272b9da3 Merge branch 'develop' into debug-api-endpoint 2022-05-05 19:17:57 -03:00
andres-portainer
943c0a6256 Avoid allocating on nil values. 2022-05-05 19:12:06 -03:00
Dakota Walsh
3de585fe17 fix(extension): extend JWT auth token expiration for extension EE-3065 (#6881)
The default expiration time of 8 hours does not make sense in the
context of the docker desktop extension. This adds a new feature flag
which can be enabled with `export DOCKER_EXTENSION=1` and when 
present will set the expiration time to 99 years.

I've set this flag in the docker-compose.yml we use when building our
docker extension.
2022-05-06 09:52:47 +12:00
andres-portainer
d245e196c1 Merge branch 'debug-api-endpoint' of github.com:portainer/portainer into debug-api-endpoint 2022-05-05 18:45:47 -03:00
andres-portainer
d6abf03d42 Cache the Settings object. 2022-05-05 18:44:11 -03:00
andres-portainer
f98585d832 Avoid a call to Endpoint().GetNextIdentifier(). 2022-05-05 18:43:16 -03:00
deviantony
bc3e973830 Merge branch 'develop' into debug-api-endpoint 2022-05-05 21:31:20 +00:00
Chaim Lev-Ari
c732ca2d2f fix(edge): allow more options for url [EE-2975] (#6781) 2022-05-05 10:03:24 +03:00
Chaim Lev-Ari
d4c2ad4a57 fix(edge/aeec): add explanation about PORTAINER_EDGE_ID [EE-3056] (#6874) 2022-05-05 10:02:34 +03:00
deviantony
0292523855 refactor(edge): rollback to original error message 2022-05-04 18:30:05 +00:00
deviantony
044d756626 feat(edge): remove logrus calls 2022-05-04 18:28:31 +00:00
deviantony
a9c0c5f835 feat(edge): rollback go routine changes 2022-05-04 18:13:58 +00:00
deviantony
2b0a519c36 feat(edge): update edge status logic 2022-05-04 01:42:55 +00:00
yi-portainer
871da94da0 * remove endpoint update for testing 2022-05-04 12:07:27 +12:00
andres-portainer
81faf20f20 Add pprof handlers. 2022-05-03 19:42:26 -03:00
deviantony
b8757ac8eb Merge branch 'develop' into debug-api-endpoint 2022-05-03 04:34:27 +00:00
deviantony
b927e08d5e feat(http): add debug logs 2022-05-03 04:33:50 +00:00
wheresolivia
bf59ef50a3 add data-cy to application creation info elements (#6871) 2022-05-02 14:06:54 +12:00
Matt Hook
840a3ce732 switch natural sort lib for a better one (#6862)
Switched to better natural sorting package
2022-05-02 12:37:26 +12:00
Oscar Zhou
f7780cecb3 feat(ci/security): add code dependency security scan and docker image vulnerability scan [EE-2537] (#6853)
This PR supports to scan code security of js and golang dependencies and image vulnerability of locally built docker image
2022-05-02 12:09:45 +12:00
deviantony
e2188edc9d feat(http): update logging format 2022-04-30 20:05:16 +00:00
deviantony
97d2a3bdf3 feat(http): add debug logs in 2022-04-30 19:45:46 +00:00
sunportainer
24c61034c1 fix endpoints tag display issue (#6851) 2022-04-30 16:30:40 +08:00
Richard Wei
95b3fff917 fix(filter): EE-2972 - fix filter selector css EE-2972 (#6858)
* fix selector css style

* fix selector css
2022-04-29 15:06:43 +12:00
itsconquest
0f52188261 fix(home): fix styles of edit button [EE-3006] (#6803)
* fix(home): fix styles of edit button [EE-3006]

* fix(home): EE-3006 fix styles of edit button

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2022-04-29 11:01:05 +12:00
itsconquest
b1b0a76465 fix(edge): fix formatting of scripts for release [EE-2987] (#6794)
* fix(edge) fix formatting for release [EE-2987]

* fix(edge) EE-2987 fix edge agent command formatting

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2022-04-29 09:44:34 +12:00
andres-portainer
8a6024ce9b fix(edge-stacks): add an endpoint to delete the status of an edge stack EE-2432 (#6551) 2022-04-28 16:50:23 -03:00
Richard Wei
61a3bfe994 fix clear all button text vertical align (#6833) 2022-04-28 10:18:44 +12:00
Chaim Lev-Ari
842044e759 chore(app): add typescript check and fix errors [EE-3014] (#6822) 2022-04-27 14:10:20 +03:00
Prabhat Khera
b3e035d353 pass tagsPartialMatch query param on home screen (#6842) 2022-04-27 17:27:35 +12:00
Prabhat Khera
33f433ce45 fix status filter (#6827) 2022-04-27 11:40:23 +12:00
itsconquest
abb79ccbeb fix(settings): fix logic for showing https section [EE-3008] (#6805) 2022-04-27 10:48:40 +12:00
cong meng
c340b62f43 fix: EE-3019 add space on top copy button (#6819) 2022-04-27 10:10:49 +12:00
Chaim Lev-Ari
bbb096412d fix(edge): show edge environment in edge views [EE-2997] (#6795) 2022-04-26 14:25:20 +03:00
Chaim Lev-Ari
141a530e28 chore(deps): add tailwindcss [DTD-29] (#6604) 2022-04-26 08:16:46 +03:00
Chaim Lev-Ari
d08b498cb9 refactor(edge): use react poll freq field [EE-2614] (#6757) 2022-04-26 08:14:48 +03:00
Prabhat Khera
bebee78152 fix(home): fix home page filters EE-2972 (#6789) 2022-04-26 12:17:36 +12:00
andres-portainer
5b77edb76d fix(aeec): enforce non-empty EdgeIDs for global key environment retrieval EE-3013 (#6808) 2022-04-25 11:35:14 -03:00
Richard Wei
bcec6a8915 fix add rewrite annotation should not available for traefik (#6799) 2022-04-22 20:02:53 +12:00
Chaim Lev-Ari
3496d5f00b test push 2022-04-22 10:17:03 +03:00
itsconquest
4ee5ae90e7 fix(user-settings): prevent autofocus on access tokens for release [EE-2978] (#6790) 2022-04-22 11:44:54 +12:00
Chaim Lev-Ari
4180e41fa1 fix(edge): generate token when loading settings [EE-2988] (#6793) 2022-04-21 19:18:49 +03:00
Chaim Lev-Ari
5289e4d66b fix(edge): generate token when loading settings [EE-2988] (#6792) 2022-04-21 19:18:42 +03:00
Matt Hook
ace162ec1c bump versions (#6782) 2022-04-20 17:19:08 +12:00
Richard Wei
a9887d4a31 Homepage Filter Component (#6762) 2022-04-20 17:04:05 +12:00
Richard Wei
8ce3e7581b fix: wildcard with hostname validation error issue EE-2101 (#6741)
* fix wildcard validation error
2022-04-20 17:02:21 +12:00
Richard Wei
9de0704775 feat(ingress): support-regex-with-k8s-ingress EE-2644 (#6748)
* support regex with k8s ingress

* remove text for rewrite to /

* added tooltip
2022-04-20 16:45:20 +12:00
cong meng
e20c34e12a feat(password) EE-2690 update the text of force password change hint (#6780)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2022-04-20 11:01:07 +12:00
Chaim Lev-Ari
e217ac7121 feat(edge): show correct heartbeat and sync aeec changes [EE-2876] (#6769) 2022-04-19 21:43:36 +03:00
sunportainer
76d1b70644 fix(volume): prevent bind mounts and allow named volumes [EE-2364] (#6771)
* check bindmounts via absolute path

* check bindmounts via absolute path
2022-04-19 20:05:16 +08:00
Stéphane Busso
360701e256 feat(docker-desktop-extension): Make Portainer compatible with Docker Desktop Extension EE-2747 (#6644)
* Initial extension build

* Add auto login

fix auto auth

add some message

Add extension version

Double attempt to login

Add auto login from jwt check

Add autologin on logout

revert sidebar

Catch error 401 to relogin

cleanup login

Add password generator

Hide User block and collapse sidebar by default

hide user box and toggle sidebar

remove defailt dd

Integrate extension to portainer

Move extension to build

remove files from ignore

Move extension folder

fix alpine

try to copy folder

try add

Change base image

move folder extension

ignore folder build

Fix

relative path

Move ext to root

fix image name

versioned index

Update extension on same image

Update mod

* fix kubeshell baseurl

* Fix kube shell

* move build and remove https

* Tidy mod

* Remove space

* Fix hash test

* Password manager

* change to building locally

* Restore version variable and add local install command

* fix local dev image + hide users & auth

* Password manageListen on locahost onlyr

* FIxes base path

* Hide only username

* Move default to constants

* Update app/portainer/components/PageHeader/HeaderContent.html

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>

* fix 2 failing FE tests [EE-2938]

* remove password autogeneration from v1

* fix webhooks

* fix docker container console and attach

* fix default for portainer IP

* update meta, dockerfile and makefile for new ver

* fix basepath in kube and docker console

* revert makefile changes

* add icon back

* Add remote short cut command

* make local methods the default

* default to 0.0.0 for version for local development

* simplify make commands

* small build fixes

* resolve conflicts

* Update api/filesystem/write.go

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>

* use a more secure default pass

Co-authored-by: itsconquest <william.conquest@portainer.io>
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
2022-04-19 13:10:42 +12:00
Chaim Lev-Ari
7efdae5eee feat(endpoints): enable env vars on kube edge deploy [EE-2542] (#6620) 2022-04-17 10:34:20 +03:00
sunportainer
da9ef7dfcf fix(download):update the downloads files directory EE-2473 (#6734)
* update the downloads files directory
2022-04-17 11:04:04 +08:00
Chao Geng
69c34cdf0c EE-2767 Correct sidebar hovering info (#6750) 2022-04-17 09:33:58 +08:00
LP B
030b3d7c4d fix(edge-jobs): HTTP 404 on file upload (#6671)
* fix(edge-jobs): HTTP 404 on file upload

* fix(edge-jobs): state 'edge job' in message on edge job removal instead of 'stack'

* fix(api/edge-jobs): save changes on edge-jobs update
2022-04-14 18:44:04 +02:00
Chaim Lev-Ari
355674cf22 fix(datastore): fix test for EdgePortainerUrl [EE-2967] (#6765) 2022-04-14 17:16:10 +03:00
Chaim Lev-Ari
85a7b7e0fc refactor(edge): move edge deploy script to react [EE-2689] (#6747) 2022-04-14 13:14:23 +03:00
Prabhat Khera
328ce2f995 fix migration test (#6763) 2022-04-14 18:57:17 +12:00
Prabhat Khera
e4241207cb fix(database): db migration improvements EE-2688 (#6662) 2022-04-14 16:25:13 +12:00
cong meng
85ad4e334a feat(password) EE-2690 enforce strong password policy (#6751)
* feat(password) EE-2690 enforce strong password policy

* feat(password) EE-2690 disable create user button if password is not valid

* feat(password) EE-2690 show force password change warning only when week password is detected

* feat(password) EE-2690 prevent users leave account page by clicking add access token button

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2022-04-14 13:45:54 +12:00
Dmitry Salakhov
9ebc963082 fix: bump golang deps to resolve CVEs (#6755) 2022-04-14 10:17:00 +12:00
andres-portainer
3178787bc1 feat(edge): implement automatic edge environment creation EE-2848 (#6754) 2022-04-13 10:08:26 -03:00
fhanportainer
b08e0b0235 feat(git): added regex to validate compose file extension (#6731)
* feat(git): added regex to validate compose file extension

* feat(git): copy change

* feat(git): updated icon
2022-04-13 21:57:39 +12:00
LP B
aac2aca912 fix(api/endpoint): refresh kubernetes client cache on endpoint update (#6752) 2022-04-12 17:05:15 +02:00
Chao Geng
f707c90cd3 fix(agent): take agent_secret into account EE-2128 (#6379)
* EE-2128 take agent_sceret into account

* EE-2128 align output code

* EE-2128 fix copy command error

* EE-2128 align code

* EE-2128 fix typo

* Update endpoint.html

remove glint auto changes

* EE-2128 Format html with Prettier

* EE-2128 Adjust UI for dark mode and adopt AGENT_SECRET on k8s automatically

* EE-2128 fix bug created by merge

* EE-2128 Move the initailization of AGENT_SECRET to main.go

* EE-2128 read AGENT_SECRET when settings is initializing
2022-04-12 18:45:58 +08:00
andres-portainer
3eea3e88bc fix(chisel): downgrade back to the previous version of Chisel EE-2718 (#6745) 2022-04-11 14:32:59 -03:00
Chaim Lev-Ari
13faa75a2d fix(endpoints): show edge script when unassociated [EE-2842] (#6730) 2022-04-11 11:26:13 +03:00
sunportainer
287107e8da update go.mod/go.sum (#6732) 2022-04-11 10:26:25 +08:00
Chaim Lev-Ari
2535887984 chore(format): format codebase [EE-2935] (#6746) 2022-04-10 14:05:31 +03:00
Marcelo Rydel
f12c3968f1 feat(edge): edgeStacks and edgeJobs operations small refactors [EE-2744] (#6648) 2022-04-08 11:27:38 -03:00
Dmitry Salakhov
6419e7740a fixed direct dependencies (#6565) 2022-04-08 10:27:58 +12:00
LP B
298e3d263e feat(registry): enforce name uniqueness for registries (#6709)
* feat(app/registries): add name uniqueness validation on registry creation

* feat(api/registry): enforce name uniqueness on registry creation

* feat(api/registry): enforce name uniqueness on registry update

* feat(app/registry): enforce name uniqueness on registry update
2022-04-07 22:58:26 +02:00
andres-portainer
9ffaf47741 fix(endpoint-status): add a redirect to keep backwards compatibility EE-2932 (#6735) 2022-04-07 11:33:14 -03:00
Marcelo Rydel
dff74f0823 feat(ssl): enable mTLS certificates [EE-2617] (#6612) 2022-04-07 11:32:00 -03:00
Marcelo Rydel
f9f937f844 feat(registries): Registry browser for non-admins [EE-2459] (#6549)
* feat(registries): allow non-admin users to see environment registries

* remove unused function

* fix error message

* fix test

* fix imports order

* feat(registry): check access first, add parameters name

* use registryID

* fix(sidebar): allow standard users to see endpoint registries view

Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2022-04-07 15:22:31 +02:00
Marcelo Rydel
77e48bfb74 chore(endpoint/edge): small refactor and move endpoint status handler [EE-2710] (#6637) 2022-04-07 09:17:36 -03:00
Dmitry Salakhov
f4ac6f8320 update helm to 3.8.0 (#6564) 2022-04-07 13:30:00 +12:00
Oscar Zhou
bf8b44834a fix(timeout): change the url of the documentation link on timeout page (#6729) 2022-04-07 13:16:28 +12:00
Dakota Walsh
3c98bf9a79 fix(theme): apply theme without saving (#6695) 2022-04-06 09:08:35 +12:00
Prabhat Khera
e1df46b92b add missed migration for DB version 36 (#6678) 2022-04-06 09:07:51 +12:00
Richard Wei
7e28b3ca3f fix issue on editing app with persisted folder (#6646)
Co-authored-by: Richard Wei <dgui.wei@gmail.com>
2022-04-06 05:42:01 +12:00
Oscar Zhou
2059a9e064 feat(adminmonitor): redirect to timeout page if admin is not created in 5 mins [EE-2691] (#6688)
This PR solves the issue that the Portainer instance will be always accessible in certain cases, like `restart: always` setting with docker run, even if the administrator is not created in the first 5 minutes. 
The solution is that the user will be redirected to a timeout page when any actions, such as refresh the page and click button, are made after administrator initialisation window(5 minutes) timeout.
2022-04-05 16:29:57 +12:00
LP B
167825ff3f feat(registries): update registry Password wording to Access Token for Dockerhub (#6557) 2022-04-04 22:54:13 +02:00
LP B
f154e6e0f1 style(app): white text in high contrast for service update dropdown text (#6660) 2022-04-04 15:57:56 +02:00
sunportainer
311129e746 fix(docker):show error for offline endpoint (#6702) 2022-04-04 18:24:47 +08:00
Chao Geng
f59459f936 EE-2463 When add agent to k8s with AGENT_SECRET, AGENT_SECRET should be configured on both side. otherwise, it will get unknown error. this error should be "agent already paired with another Portainer instance" (#6679) 2022-04-01 14:40:44 +08:00
sunportainer
ee90fffce1 fix(template) add notification when delete (#6675) 2022-04-01 14:40:08 +08:00
wheresolivia
4ddd6663f5 add data-cy to mark namespace system button (#6714) 2022-04-01 13:30:56 +13:00
Richard Wei
ec3d7026d4 fix(service): fix service name starting from zero EE-2807 (#6694)
* fix service name index
2022-03-31 10:34:29 +13:00
Richard Wei
fb7f24df9c fix domain name selector issue (#6696)
Co-authored-by: Richard Wei <dgui.wei@gmail.com>
2022-03-30 15:50:11 +13:00
Marcelo Rydel
8860d72f70 fix(edge/jobs): fix get edge job file content [EE-2702] (#6622) 2022-03-28 12:02:09 -03:00
sunportainer
b846c8e6d2 fix(stack): git stack redeploy issue EE-2737 (#6667)
* fix(stack) git stack redeploy issue
2022-03-28 21:31:03 +08:00
sunportainer
379f9e2822 fix(swagger):add swagger for X-Registry-Auth EE-2408 (#6664)
* add swagger for X-Registry-Auth

* Update api-description.md
2022-03-28 21:30:19 +08:00
cong meng
3579b11a8b fix(k8s) deploying manifest using default namespace EE-2104 (#6385)
* fix(k8s) deploying manifest using default namespace EE-2104
2022-03-24 21:28:53 +13:00
wheresolivia
4377aec72b feat(cy):add data-cy to add kube application publishing mode dropdown… (#6687)
* feat(cy):add data-cy to add kube application publishing mode dropdown list, rename the create service button data-cy

* fix prettier issues
2022-03-22 14:54:17 +13:00
Marcelo Rydel
c486130a9f fix(kube): Use KubeClusterAccessService for Helm operations [EE-2500] (#6559) 2022-03-21 09:51:29 -03:00
Chaim Lev-Ari
cf7746082b fix(stacks): show force pull image for git stacks [EE-2579] (#6607) 2022-03-21 14:35:31 +02:00
andres-portainer
1ab65a4b4f fix(offlinegate): fix data race in offlinegate EE-2713 (#6626) 2022-03-18 13:20:10 -03:00
andres-portainer
a66e863646 fix(boltdb): upgrade to the latest version to avoid problems with the race detector EE-2729 (#6638) 2022-03-18 13:16:31 -03:00
Marcelo Rydel
d962c300f9 fix(containers/datatable): disable autoreset expanded and selected rows [EE-2347] (#6563) 2022-03-17 14:55:11 -03:00
Richard Wei
9aeedf1bfa fix(ingress): fix-multiple-route-on-same-ingress EE-2597 (#6609)
* fix multiple route for same ingress & improvement for multiple ingress controller
2022-03-17 10:25:36 +13:00
andres-portainer
98d8cd99fb fix(chisel): upgrade chisel to v1.7.7 to fix a data race EE-2718 (#6650) 2022-03-16 12:17:56 -03:00
andres-portainer
226ffdcd20 fix(snapshots): fix a data race in the snapshot code EE-2717 (#6654) 2022-03-16 11:27:28 -03:00
andres-portainer
78150a738f fix(scheduler): fix a data race in the scheduler EE-2716 (#6629) 2022-03-16 10:33:15 -03:00
andres-portainer
ecf5e90783 fix(admin-monitor): fix a data race in the admin monitor EE-2761 (#6658) 2022-03-16 09:13:45 -03:00
Chaim Lev-Ari
f63b07bbb9 refactor(access-control): create access-control-panel component [EE-2345] (#6486) 2022-03-16 08:35:32 +02:00
Chao Geng
07294c19bb fix(k8s/application): check name unique in k8s cluster (#6610)
* EE-2353 Check unique name when creating new deployment in kubernetes

* EE-2353 fix warning from gofmt

* EE-2353 add miss methon in kubernetes_mock.go

* EE-2353 add missing space

* EE-2353 Use kubernetes cli to instead exec.command

* EE-2353 remove useless parameter

* EE-2353 remove unnecessary log in handle

* EE-2353 fix gofmt warning

* EE-2353 use ListOptions to filter the list

* EE-2353 add function description

* EE-2353 fix error

* Update api/kubernetes/cli/deploment.go

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>

* EE-2353 change function name to HasStackName

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
2022-03-16 08:32:12 +08:00
andres-portainer
f8cbb54ba5 fix(tunnels): fix a deadlock with the tunnels EE-2751 (#6649) 2022-03-15 12:37:09 -03:00
andres-portainer
f8fd28bb61 fix(scheduler): fix a data race in a scheduler unit test EE-2715 (#6628) 2022-03-15 09:52:58 -03:00
andres-portainer
78f7cd0d6c fix(adminmonitor): fix a data race in a unit test EE-2714 (#6627) 2022-03-15 09:52:41 -03:00
Chaim Lev-Ari
9a42d4c506 fix(auth/ldap): show server url [EE-2069] (#6651) 2022-03-15 07:13:39 +02:00
itsconquest
f2c48409e0 refactor(azure/aci): migrate sidebar to react [EE-2569] (#6593)
* refactor(azure/aci): migrate sidebar to react [EE-2569]

* add test files

* add story

* fix(sidebar): get styles from sidebar

* make suggested changes + update icon story

* use template in second story + change some english

* use camel case in test

* use icon instead of span

* refactor(types): use existing environmentid type

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2022-03-14 19:26:30 +13:00
Oscar Zhou
5188ead870 fix(home): fix homepage edge heartbeat judgement [EE-2041] (#6624)
* fix(home): judge LastCheckInDate with QueryDate for heartbeat

* refactor(environments): remove deprecated variable homepageLoadTime

* style(environments): run yarn format

Co-authored-by: sam@gemibook <huapox@126.com>
2022-03-14 14:53:23 +13:00
Chaim Lev-Ari
f1ea2b5c02 chore(account): write tests for CreateAccessToken [EE-2561] (#6578) 2022-03-13 09:14:41 +02:00
KyKlen
b7d18ef50f fix(volumes): add addr field in options when creating a CIFS volume [EE-2349] (#6359) 2022-03-10 13:06:52 -03:00
sunportainer
20405e9803 fix(docker/service): send registry id on update EE-2061 (#6606) 2022-03-10 07:35:11 +02:00
Chaim Lev-Ari
0f3c7b1424 refactor(home): migrate view to react [EE-1810] (#6314)
* refactor(http): parse axios errors (#6325)

* refactor(home): use endpoint-list as react component [EE-1814] (#6060)

* refactor(home): use endpoint-list as react component

fix(home): add missing features and refactors

- kubebutton
- group name
- poll when endpoint is off
- state management

refactor(endpoints): use stat component

fix(endpoints): add space between items

refactor(endpoints): move stats to components

refactor(endpoints): fetch time

refactor(home): move logic

refactor(home): move fe render logic

refactor(settings): use vanilla js for publicSettings

refactor(kube): remove angular from kube config service

feat(home): add kubeconfig button

feat(home): send analytics when opening kubeconfig modal

fix(home): memoize footer

refactor(home): use react-query for loading

fix(home): show correct control for kubeconfig modal

refactor(home): use debounce

refactor(home): use new components

refactor(home): replace endpoints with environments

refactor(home): move endpoint-list component to home

fix(home): show group name

refactor(home): use switch for environment icon

fix(kubeconfig): fix default case

refactor(axios): use parse axios error

refactor(home): use link components for navigate

fix(home): align azure icon

refactor(home): refactor stats

refactor(home): export envstatusbadge

refactor(home): remove unused bindings

* chore(home): write tests for edge indicator

* chore(home): basic stories for environment item

* style(settings): reformat

* fix(environments): add publicurl

* refactor(home): use table components

* refactor(datatables): merge useSearchBarState

* refactor(home): fetch group in env item

* chore(tests): basic tests

* chore(home): test when no envs

* refactor(tags): use axios for tagService

* refactor(env-groups): use axios for getGroups

* feat(app): ui-state context provider

* refactor(home): create MotdPanel

* refactor(app): create InformationPanel

* feat(endpoints): fetch number of total endpoints

* refactor(app): merge hooks

* refactor(home): migrate view to react [EE-1810]

fixes [EE-1810]

refactor(home): wip use react view

feat(home): show message if no endpoints

refactor(home): show endpoint list

refactor(home): don't use home to manage link

refactor(home): move state

refactor(home): check if edge using util

refactor(home): move inf panels

chore(home): tests

refactor(home): load groups and tags in env-item

refactor(settings): revert publicSettings change

refactor(home): move confirm snapshot method

* fix(home): show tags

* fix(environments): handle missing snapshots

* fix(kube/volumes): fetch pesistent volume claims

* refactor(kube): remove use of endpointProvider

* refactor(endpoints): set current endpoint

* chore(home): add data-cy for tests

* chore(tests): mock axios-progress-bar

* refactor(home): move use env list to env module

* feat(app): sync home view changes with ee

* fix(home): sort page header

* fix(app): fix tests

* chore(github): use yarn cache

* refactor(environments): load list of groups

* chore(babel): remove auto 18n keys extraction

* chore(environments): fix tests

* refactor(k8s/application): use current endpoint

* fix(app/header): add margin to header

* refactor(app): remove unused types

* refactor(app): use rq onError handler

* refactor(home): wrap element with button
2022-03-08 14:14:23 +02:00
sunportainer
c442d936d3 fix(compose):filter out symlink in custom template EE-1928 (#6579)
* fix prevent symlink in customtemplate
2022-03-04 12:05:34 +08:00
testA113
0cd164bada add data-cy attributes (#6623) 2022-03-04 14:56:04 +13:00
itsconquest
ee42e44246 refactor(edge-compute): remove toggle from settings (release) [EE-2686] (#6619) 2022-03-03 13:31:01 +13:00
itsconquest
6695d75468 fix(endpoints): fix broken style (release) [EE-2659] (#6613)
* fix(endpoints): fix broken style (release) [EE-2659]

* fix(endpoints): show margin under env var field [EE-2659]

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2022-03-03 11:48:30 +13:00
Prabhat Khera
eb6cdf1229 created bucket if not exists during restore sequence (#6614) 2022-03-03 09:10:26 +13:00
andres-portainer
a3b1466b96 fix(tunnel): fix data race on tunnels EE-2577 (#6601) 2022-03-02 13:51:22 -03:00
Marcelo Rydel
8b7dcf20bf feat(db): add CreateObjectWithStringId function [EE-2612] (#6611) 2022-03-02 09:22:03 -03:00
Prabhat Khera
14ed6ed2a3 DB upgrade failes if bucket does not exists (#6608) 2022-03-01 10:31:33 +13:00
Chaim Lev-Ari
9f4549212d fix(auth): remove caching of user (#6591) 2022-03-01 09:38:16 +13:00
Chao Geng
37209918ad fix(docker/stacks): upgrade docker-compose-wrapper [EE-1975] (#6598)
* updated docker-compose-wrapper

* keep the same
2022-02-28 17:24:15 +08:00
Chaim Lev-Ari
aefa34d6d2 fix(k8s/application): allow app name to start with alphabetic character [EE-2596] (#6603)
fixes [EE-2596]
2022-02-28 07:15:49 +02:00
Hao Zhang
eaffde39f6 fix(stack): incorrect stack name (#6587) 2022-02-27 16:04:48 +08:00
Hao Zhang
d71d291895 fix(stack): git repo auto update not working (#6573) 2022-02-27 16:03:05 +08:00
itsconquest
a894e3182a refactor(azure/aci): migrate dashboard view to react [EE-2189] (#6518)
* refactor(azure/aci): migrate dashboard view to react [EE-2189]

* move aggregate function to azure utils file

* fix type

* introduce dashboard item component

* add error notificatons

* hide resource groups widget if failed to load

* make dashboard a default export

* revert mistake

* refactor based on suggestions

* use object for error data instead of array

* return unused utils file

* move length calculations out of return statement

* only return first error of resource groups queries

* refactor imports/exports, fix bug with errors & add test

* WIP dashboard tests

* allow mocking multiple resource groups

* test for total number of resource groups

* update lock file to fix lint action issue

* finish dashboard tests

* dashboarditem story

* fix(auth): remove caching of user

* add option for link to dashboard item

* rename dashboard test case to match file

* remove optional link and update storybook

* create aria label based on already provided text

* change param name to be clearer
2022-02-25 12:22:56 +13:00
Chaim Lev-Ari
ff7847aaa5 chore(git): ignore prettier commits on git blame (#6584)
* chore(git): ignore prettier commits on git blame

* chore(vscode): fix launch command
2022-02-22 16:27:35 +02:00
Matt Hook
a89c3773dd fix(datastore): export/import the bolt sequence number EE-2451 (#6571)
* Implement setter/getter for the sequence

* import/export counts

* fix go tests.  rename vars

* Improved and simplified the logic. Made it more generic

* Remove unused methods

* remove unused methods

* not part of branch fix
2022-02-22 09:53:17 +13:00
Hao Zhang
5d75ca34ea fix(stack): git force pull image toggle only for non-kubernetes git based stacks (#6574) 2022-02-21 08:43:22 +08:00
Marcelo Rydel
d47a9d590e fix(kube): namespace parameter is not used in kube redeploy (#6569) 2022-02-18 16:36:20 +13:00
Anthony Lapenna
bd679ae806 feat(endpoint): add an input to source env vars [EE-2436] (#6517)
* feat(endpoint): add an input to source env vars

* fix(endpoint): fix invalid version in deployment instructions

* fix(endpoint): fix copy Edge command

* fix(endpoint): fix invalid Edge deployment instruction

* feat(endpoint): add missing parameter to edge deploy script

* feat(edge): use temporary manifest url

* refactor(endpoint): update method and placeholder

* fix(endpoint): fix missing agent name in Edge deployment instructions on Swarm

* fix(endpoint): fix invalid Edge deployment instructions for Kubernetes

* fix(build): commit yarn.lock

* chore(deps): run yarn

* feat(endpoint): do not support kubernetes with Edge env vars

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2022-02-17 10:25:59 +13:00
LP B
5de7ecb5f0 chore(deps): freeze lockfile on Github Actions (#6570) 2022-02-16 18:20:15 +01:00
Marcelo Rydel
b3cd9c69df fix(edge/settings): render view after loading settings [EE-2532] (#6560) 2022-02-15 18:26:42 -03:00
Chaim Lev-Ari
73311b6f32 fix(edge/devices): make actions button larger [EE-2471] (#6542)
* fix(edge/devices): make actions button larger [EE-2471]

fixes [EE-2471]

* fix(edge/devices): fix table-actions-title padding

Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
2022-02-16 08:38:24 +13:00
Sven Dowideit
93ddcfecd9 fix(templates): show docker-compose app templates when in swarm mode [EE-2117] (#6177)
* fix(templates): EE-2117: show docker-compose app templates when in swarm mode and the user selects 'showContainers

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>

* fix(templates): keep original behavior for standalone

* fix(templates): display all templates on Swarm

* refactor(templates): update method name

Co-authored-by: deviantony <anthony.lapenna@portainer.io>
2022-02-15 07:30:02 +13:00
Marcelo Rydel
2bffba7371 fix(edge): only show expand row for Edge Devices with AMT activated [EE-2489] (#6519) 2022-02-14 11:44:55 -03:00
Hao Zhang
37ca62eb06 feat(webhook): teasers of pull images and webhook for EE EE-1332 (#6278)
* feat(webhook): teasers of pull images and webhook for EE
2022-02-14 21:51:43 +08:00
Chaim Lev-Ari
fa208c7f2a docs(github): fix slack link [EE-2438] (#6541)
Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
2022-02-11 10:07:14 -03:00
testA113
6fac3fa127 add data-cy attributes for backup/restore (#6546)
Co-authored-by: testA113 <42307911+aliharriss@users.noreply.github.com>
2022-02-11 15:24:44 +13:00
deviantony
171392c5ca chore(dev): update vscode example 2022-02-10 22:04:27 +00:00
Marcelo Rydel
d48ff2921b fix(edge): show KVM connect button, remove automatic useEffect [EE-2520] (#6540) 2022-02-10 14:23:09 -03:00
Chaim Lev-Ari
3165d354b5 fix(settings): clear helm url if requested [EE-2494] (#6526)
* fix(settings): clear helm url if requested [EE-2494]

fix [EE-2494]

before this PR, helm url would clear when updating settings, if the helm url key wasn't provided.
in this PR, it will be changed only if required

* fix(settings): allow empty helm repo

* chore(deps): run yarn

* fix(settings): set helm repo url
2022-02-10 06:03:46 +02:00
Chaim Lev-Ari
9c2dbac479 fix(services): show task actions EE-2505 2022-02-09 11:49:44 +13:00
Anthony Lapenna
318844226c refactor(storidge): remove Storidge support from backend [EE-2450] (#6511)
* refactor(storidge): remove Storidge support from backend

* refactor(storidge): remove Storidge support from backend

* refactor(storidge): remove Storidge support from frontend
2022-02-09 05:47:11 +13:00
Chaim Lev-Ari
e96f63023e chore(deps): upgrade libhttp [EE-2145] (#6530)
closes [EE-2145]
2022-02-08 07:09:14 +02:00
dependabot[bot]
1765b99336 chore(deps): bump bl from 1.2.2 to 1.2.3 (#4441)
Bumps [bl](https://github.com/rvagg/bl) from 1.2.2 to 1.2.3.

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 16:31:14 +13:00
andres-portainer
74a0d4c12e fix(fdo): change 'http' to 'https' in the placeholder text EE-2479 (#6516) 2022-02-02 20:35:56 -03:00
testA113
3372f78cbf fix font weight for firefox (#6514) 2022-02-02 12:32:46 +13:00
andres-portainer
fe082f762f fix(fdo): add suffix to the device name supplied to /fdo/configure EE-2469 (#6505) 2022-02-01 19:38:50 -03:00
Prabhat Khera
a8d3cda3fa Fix(db): needs encryption migration function fixed EE-2414 (#6494)
* fix(db) NeedsEncryptionMigration EE-2414
* fix for case where we started encrypted and restore unencrypted.  We don't want to have two databases
* fix(db): handle decryption error EE-2466

Co-authored-by: Matt Hook <hookenz@gmail.com>
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2022-02-02 09:53:59 +13:00
testA113
ad7f87122d fix(tooltip): inconsistent tooltip component EE-2472 (#6508)
* Fixed tooltip styling

* match old tooltip styling

* Match font size
2022-02-02 08:42:19 +13:00
Chaim Lev-Ari
6f6f78fbe5 refactor(azure/aci): migrate create view to react [EE-2188] (#6371) 2022-02-01 19:38:45 +02:00
andres-portainer
1bb02eea59 fix(db): handle decryption error EE-2466 (#6499) 2022-02-01 11:48:26 -03:00
Marcelo Rydel
cf459a2d28 fix(ssl): default httpEnabled to false [EE-2465] (#6495) 2022-02-01 09:14:43 -03:00
Chaim Lev-Ari
7d91ab72e1 fix(agent): add agent header [EE-2433] (#6484)
* fix(agent): add agent header [EE-2433]

fix [EE-2433]

* fix(containers): reload current endpoint id
2022-02-01 09:37:12 +02:00
andres-portainer
cb804e8813 fix(edge): change the edge menu to work in dark mode EE-2462 (#6488)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2022-01-31 19:21:47 -03:00
andres-portainer
0973808234 fix(fdo): change the owner service connection message EE-2457 (#6490) 2022-01-28 10:06:21 -03:00
Marcelo Rydel
edd5193100 fix(settings): updateSettingsFromFlags only if dataStore is new [EE-2397] (#6475) 2022-01-28 09:28:34 -03:00
Matt Hook
0ad66510a9 this was checked in by mistake... removing. (#6436) 2022-01-28 09:13:34 +13:00
Prabhat Khera
5a6cd2002d fix base url in axios (#6460) 2022-01-28 09:00:01 +13:00
Chaim Lev-Ari
1fbf13e812 fix(k8s/app): populate ingress details [EE-2445] (#6463)
* fix(k8s/app): populate ingress details [EE-2445]

fix [EE-2445]

* fix(k8s/app): check if there are ingresses
2022-01-27 08:37:46 +02:00
Hao Zhang
a9406764ee fix(service): webhook vulnerability for passing an invalid image tag EE-2121 (#6269)
* fix(service): webhook vulnerability for passing an invalid image tag
2022-01-27 08:38:29 +08:00
Dmitry Salakhov
dfb0ba9efe Add PR template (#4837)
* Add PR template

* add link to Jira

* use jira syntax to esteblish a link

* ask to reference jira in PR title
2022-01-27 11:54:43 +13:00
Marcelo Rydel
df2269a2fe chore(lint): run yarn format (#6476) 2022-01-26 12:22:58 -03:00
andres-portainer
8b4a74f06e fix(fdo): generate an edgeID when the 'Enforce environment ID' setting is disabled EE-2446 (#6465) 2022-01-25 15:25:27 -03:00
andres-portainer
48f2e7316a fix(fdo): cancel the action in progress on error EE-2447 (#6469) 2022-01-25 11:46:13 -03:00
Marcelo Rydel
b76bcf0ee7 fix(images): fix registryModal [EE-2426] (#6442) 2022-01-25 09:13:36 -03:00
sunportainer
24893573aa feat/ee-1991/validate-k8s-workload (#6302) 2022-01-25 18:59:09 +08:00
sunportainer
118809a9c0 Fix(kube):fix kube show rounding issue EE-2115 (#6300)
* fix/ee-2115/kube-show-rounding
2022-01-25 15:03:14 +08:00
Richard Wei
61be10bb00 fix input text color (#6468) 2022-01-25 15:56:25 +13:00
cong meng
4bd3f61ce6 fix(db) EE-2425 http-disabled flag does not work (#6447)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2022-01-25 09:32:31 +13:00
Richard Wei
48c2f127f8 fix(ui): fix components have unreadable text in dark mode EE-2417 (#6433)
* add styles to UsersSelector components
2022-01-25 08:49:28 +13:00
Chaim Lev-Ari
b588d901cf fix(app): skip authorizations in CE [EE-2423] (#6431)
* feat(app): check auth on ee only

* refactor(features): load edition from env var

* fix(containers): show empty message if no containers
2022-01-24 08:02:23 +02:00
Marcelo Rydel
2c4c638f46 feat(intel): Enable OpenAMT and FDO capabilities (#6212)
* feat(openamt): add AMT Devices information in Environments view [INT-8] (#6169)

* feat(openamt): add AMT Devices Ouf of Band Managamenet actions  [INT-9] (#6171)

* feat(openamt): add AMT Devices KVM Connection [INT-10] (#6179)

* feat(openamt): Enhance the Environments MX to activate OpenAMT on compatible environments [INT-7] (#6196)

* feat(openamt): Enable KVM by default [INT-25] (#6228)

* feat(fdo): implement the FDO configuration settings INT-19 (#6238)

feat(fdo): implement the FDO configuration settings INT-19

* feat(fdo): implement Owner client INT-17 (#6231)

feat(fdo): implement Owner client INT-17

* feat(openamt): hide wireless config in OpenAMT form (#6250)

* feat(openamt): Increase OpenAMT timeouts [INT-30] (#6253)

* feat(openamt): Disable the ability to use KVM and OOB actions on a MPS disconnected device [INT-36] (#6254)

* feat(fdo): add import device UI [INT-20] (#6240)

feat(fdo): add import device UI INT-20

* refactor(fdo): fix develop merge issues

* feat(openamt): Do not fetch OpenAMT details for an unassociated Edge endpoint (#6273)

* fix(intel): Fix switches params (#6282)

* feat(openamt): preload existing AMT settings (#6283)

* feat(openamt): Better UI/UX for AMT activation loading [INT-39] (#6290)

* feat(openamt): Remove wireless config related code [INT-41] (#6291)

* yarn install

* feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6292)

* feat(openamt): change kvm redirection for pop up, always enable features [INT-37] (#6293)

* feat(openmt): use .ts services with axios for OpenAMT (#6312)

* Minor code cleanup.

* fix(fdo): move the FDO client code to the hostmanagement folder INT-44 (#6345)

* refactor(intel): Add Edge Compute Settings view (#6351)

* feat(fdo): add FDO profiles INT-22 (#6363)

feat(fdo): add FDO profiles INT-22

* fix(fdo): fix incorrect profile URL INT-45 (#6377)

* fixed husky version

* fix go.mod with go mod tidy

* feat(edge): migrate OpenAMT devices views to Edge Devices [EE-2322] (#6373)

* feat(intel): OpenAMT UI/UX adjustments (#6394)

* only allow edge agent as edge device

* show all edge agent environments on Edge Devices view

* feat(fdo): add the ability to import multiple ownership vouchers at once EE-2324 (#6395)

* fix(edge): settings edge compute alert (#6402)

* remove pagination, add useMemo for devices result array (#6409)

* feat(edge): minor Edge Devices (AMT) UI fixes (#6410)

* chore(eslint): fix versions

* chore(app): reformat codebase

* change add edge agent modal behaviour, fix yarn.lock

* fix use pagination

* remove extractedTranslations folder

* feat(edge): add FDO Profiles Datatable [EE-2406] (#6415)

* feat(edge): add KVM workaround tooltip (#6441)

* feat(edge): Add default FDO profile (#6450)

* feat(edge): add settings to disable trust on first connect and enforce Edge ID INT-1 EE-2410 (#6429)

Co-authored-by: andres-portainer <91705312+andres-portainer@users.noreply.github.com>
Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2022-01-24 08:48:04 +13:00
Chaim Lev-Ari
3ed92e5fee fix(docker): delete docker resources [EE-2411] (#6414)
fixes [EE-2411]

ignore resource control object not found when deleting a docker resource
2022-01-23 09:17:31 +02:00
Chaim Lev-Ari
804fdd414e fix(stacks): migrate stack resource control [EE-2412] (#6424)
fixes [EE-2412]
2022-01-23 09:16:39 +02:00
sunportainer
661f0aad49 feat(user):logout after change password EE-1590 (#6267)
* fix(user) logout after password change
2022-01-21 08:33:43 +08:00
Richard Wei
58de8e175f add data-cy to groupform table (#6432) 2022-01-21 12:45:21 +13:00
cong meng
1e21aeb7e8 fix(bolt) EE-2415 return nil err when resource controller not found in db (#6422)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2022-01-20 13:45:53 +13:00
Richard Wei
a79aa221d3 fix error when edit pod application (#6418) 2022-01-20 08:21:03 +13:00
andres-portainer
50b2f789a3 feat(performance): add settings to tune the performance of the database EE-2363 (#6389)
* feat(performance): add settings to tune the performance of the database EE-2363

* Change panics to log.Fatals.

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2022-01-18 11:25:29 +13:00
fhanportainer
bc70198102 fix(kube): fixed kube config download info issue. (#6386) 2022-01-18 10:30:08 +13:00
Chaim Lev-Ari
1b1a50d6b5 fix(app): add github action for linting and formatting [EE-2344] (#6356) 2022-01-17 07:53:32 +02:00
Matt Hook
34cc8ea96a feat(database): add encryption support EE-1983 (#6316)
* bootstrap encryption key

* secret key message change in cli and secret key file content trimmed

* Migrate encryption code to latest version

* pull in newer code

* tidying up

* working data encryption layer

* fix tests

* remove stray comment

* fix a few minor issues and improve the comments

* split out databasefilename with param to two methods to be more obvious

* DB encryption integration (#6374)

* json methods moved under DBConnection

* store encryption fixed

* cleaned

* review comments addressed

* newstore value fixed

* backup test updated

* logrus format config updated

* Fix for newStore

Co-authored-by: Matt Hook <hookenz@gmail.com>

* Minor improvements

* Improve the export code.  Add missing webhook for import

* rename HelmUserRepositorys to HelmUserRepositories

* fix logging messages

* when starting portainer with a key (first use) http is disabled by default.  But when starting fresh without a key, http is enabled?

* Fix bug for default settings on new installs

Co-authored-by: Prabhat Khera <prabhat.khera@portainer.io>
Co-authored-by: Prabhat Khera <91852476+prabhat-org@users.noreply.github.com>
2022-01-17 16:40:02 +13:00
Hui
59ec22f706 fix(docker-compose): add logic control for docker compose force recreate EE-2356 2022-01-17 10:20:45 +13:00
Richard Wei
c47e840b37 feat(k8s): Allow mix services for k8s app EE-1791 (#6198)
allow a mix of services for k8s in ui
2022-01-17 08:37:46 +13:00
Chaim Lev-Ari
edf048570b fix(oauth): change default microsoft logout url [EE-2044] (#6324) 2022-01-16 08:58:24 +02:00
Chao Geng
b71ca2afb0 EE-1958 Set default value of auth and auto-update to off in page Manifest and stacks (#6380) 2022-01-16 00:44:20 +08:00
Hao Zhang
9ff8f42a66 feat(stack): detach git based stacks from git EE-2143 (#6307)
* feat(stack): detach git based stacks from git
2022-01-14 11:47:47 +08:00
Richard Wei
125d84cbd1 fix automatic team membership toggle issue (#6382) 2022-01-14 13:42:16 +13:00
Chaim Lev-Ari
fa798665cd chore(i18n): set extract output path (#6384) 2022-01-13 16:19:08 +02:00
Chaim Lev-Ari
95fbf7500c fix(azure): parse validation error [EE-2334] (#6341)
fixes [EE-2334]
2022-01-13 07:29:32 +02:00
Chaim Lev-Ari
584a46d9d4 fix(stacks): show stack containers [EE-2359] (#6375)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2022-01-13 07:28:49 +02:00
Chaim Lev-Ari
085762a1f4 fix(auth): prevent login for non admin for ldap and oauth [EE-648] (#5283) 2022-01-13 07:27:26 +02:00
Richard Wei
6c32edc5b5 fix background color for boxselector in dark/high contrast theme (#6378) 2022-01-13 16:55:36 +13:00
1058 changed files with 29809 additions and 19001 deletions

View File

@@ -1,3 +1,5 @@
*
!dist
!build
!metadata.json
!docker-extension/build

1
.env.defaults Normal file
View File

@@ -0,0 +1 @@
PORTAINER_EDITION=CE

View File

@@ -99,3 +99,7 @@ overrides:
'jest/globals': true
rules:
'react/jsx-no-constructed-context-values': off
- files:
- app/**/*.stories.*
rules:
'no-alert': off

5
.git-blame-ignore-revs Normal file
View File

@@ -0,0 +1,5 @@
# prettier
cf5056d9c03b62d91a25c3b9127caac838695f98
# prettier v2
42e7db0ae7897d3cb72b0ea1ecf57ee2dd694169

View File

@@ -2,7 +2,7 @@
Thanks for opening an issue on Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby.
If you are reporting a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this

View File

@@ -12,7 +12,7 @@ Thanks for reporting a bug for Portainer !
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this
@@ -47,7 +47,7 @@ You can see how [here](https://documentation.portainer.io/r/portainer-logs)
- Platform (windows/linux):
- Command used to start Portainer (`docker run -p 9443:9443 portainer/portainer`):
- Browser:
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
- Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
- Have you reviewed our technical documentation and knowledge base? Yes/No
**Additional context**

View File

@@ -4,11 +4,11 @@ about: Ask us a question about Portainer usage or deployment
title: ''
labels: ''
assignees: ''
---
Before you start, we need a little bit more information from you:
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commerical setup.
Use Case (delete as appropriate): Using Portainer at Home, Using Portainer in a Commercial setup.
Have you reviewed our technical documentation and knowledge base? Yes/No
@@ -16,7 +16,7 @@ Have you reviewed our technical documentation and knowledge base? Yes/No
You can find more information about Portainer support framework policy here: https://old.portainer.io/2019/04/portainer-support-policy/
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
-->

View File

@@ -4,14 +4,13 @@ about: Suggest a feature/enhancement that should be added in Portainer
title: ''
labels: ''
assignees: ''
---
<!--
Thanks for opening a feature request for Portainer !
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
Do you need help or have a question? Come chat with us on Slack https://portainer.io/slack/
Before opening a new issue, make sure that we do not have any duplicates
already open. You can ensure this by searching the issue list for this

4
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,4 @@
closes #0 <!-- Github issue number (remove if unknown) -->
closes [CE-0] <!-- Jira link number (remove if unknown). Please also add the same [CE-XXX] at the back of the PR title -->
### Changes:

38
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: Lint
on:
push:
branches:
- master
- develop
- release/*
pull_request:
branches:
- master
- develop
- release/*
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'yarn'
- 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

View File

@@ -0,0 +1,230 @@
name: Nightly Code Security Scan
on:
schedule:
- cron: '0 8 * * *'
workflow_dispatch:
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:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
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 js security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/js-result")
- name: Upload js result html file
uses: actions/upload-artifact@v3
with:
name: html-js-result-${{github.run_id}}
path: js-result.html
- name: Analyse the js result
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=js_result::${result}"
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:
- uses: actions/checkout@master
- name: Download go modules
run: cd ./api && go get -t -v -d ./...
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/golang@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=./api/go.mod
json: true
- name: Upload go security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-develop-result
path: snyk.json
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=table -export -export-filename="/data/go-result")
- name: Upload go result html file
uses: actions/upload-artifact@v3
with:
name: html-go-result-${{github.run_id}}
path: go-result.html
- name: Analyse the go result
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=snyk -path="/data/snyk.json" -output-type=matrix)
echo "::set-output name=go_result::${result}"
image-vulnerability:
name: Build docker image and Image vulnerability check
runs-on: ubuntu-latest
if: >-
github.ref == 'refs/heads/develop'
outputs:
image: ${{ steps.set-matrix.outputs.image_result }}
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Use golang 1.18
uses: actions/setup-go@v3
with:
go-version: '1.18'
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install packages and build
run: yarn install && yarn build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: build/linux/Dockerfile
tags: trivy-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
- name: Load docker image
run: |
docker load --input /tmp/trivy-portainer-image.tar
- name: Run Trivy vulnerability scanner
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
- name: Upload image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-develop-result
path: image-trivy.json
- name: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=table -export -export-filename="/data/image-result")
- name: Upload go result html file
uses: actions/upload-artifact@v3
with:
name: html-image-result-${{github.run_id}}
path: image-result.html
- name: Analyse the trivy result
id: set-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 summary -report-type=trivy -path="/data/image-trivy.json" -output-type=matrix)
echo "::set-output name=image_result::${result}"
result-analysis:
name: Analyse scan result
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: ${{fromJson(needs.image-vulnerability.outputs.image)}}
steps:
- name: Display the results of js, go and image
run: |
echo ${{ matrix.js.status }}
echo ${{ matrix.go.status }}
echo ${{ matrix.image.status }}
echo ${{ matrix.js.summary }}
echo ${{ matrix.go.summary }}
echo ${{ matrix.image.summary }}
- name: Send Slack message
if: >-
matrix.js.status == 'failure' ||
matrix.go.status == 'failure' ||
matrix.image.status == 'failure'
uses: slackapi/slack-github-action@v1.18.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 vulnerability check*: *${{ matrix.image.status }}*\n${{ matrix.image.summary }}\n"
}
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

233
.github/workflows/pr-security.yml vendored Normal file
View File

@@ -0,0 +1,233 @@
name: PR Code Security Scan
on:
pull_request_review:
types:
- submitted
- edited
paths:
- 'package.json'
- 'api/go.mod'
- 'gruntfile.js'
- 'build/linux/Dockerfile'
- 'build/linux/alpine.Dockerfile'
- 'build/windows/Dockerfile'
jobs:
client-dependencies:
name: Client dependency check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
outputs:
jsdiff: ${{ steps.set-diff-matrix.outputs.js_diff_result }}
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
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 js security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: js-security-scan-feat-result
path: snyk.json
- name: Download artifacts from develop branch
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: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="/data/js-snyk-develop.json" -output-type=table -export -export-filename="/data/js-result")
- name: Upload js result html file
uses: actions/upload-artifact@v3
with:
name: html-js-result-compare-to-develop-${{github.run_id}}
path: js-result.html
- name: Analyse the js diff result
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/js-snyk-feature.json" -compare-to="./data/js-snyk-develop.json" -output-type=matrix)
echo "::set-output name=js_diff_result::${result}"
server-dependencies:
name: Server dependency check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
outputs:
godiff: ${{ steps.set-diff-matrix.outputs.go_diff_result }}
steps:
- uses: actions/checkout@master
- name: Download go modules
run: cd ./api && go get -t -v -d ./...
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/golang@master
continue-on-error: true # To make sure that artifact upload gets called
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --file=./api/go.mod
json: true
- name: Upload go security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: go-security-scan-feature-result
path: snyk.json
- name: Download artifacts from develop branch
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: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=table -export -export-filename="/data/go-result")
- name: Upload go result html file
uses: actions/upload-artifact@v3
with:
name: html-go-result-compare-to-develop-${{github.run_id}}
path: go-result.html
- name: Analyse the go diff result
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=snyk -path="/data/go-snyk-feature.json" -compare-to="/data/go-snyk-develop.json" -output-type=matrix)
echo "::set-output name=go_diff_result::${result}"
image-vulnerability:
name: Build docker image and Image vulnerability check
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
outputs:
imagediff: ${{ steps.set-diff-matrix.outputs.image_diff_result }}
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Use golang 1.18
uses: actions/setup-go@v3
with:
go-version: '1.18'
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install packages and build
run: yarn install && yarn build
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: build/linux/Dockerfile
tags: trivy-portainer:${{ github.sha }}
outputs: type=docker,dest=/tmp/trivy-portainer-image.tar
- name: Load docker image
run: |
docker load --input /tmp/trivy-portainer-image.tar
- name: Run Trivy vulnerability scanner
uses: docker://docker.io/aquasec/trivy:latest
continue-on-error: true
with:
args: image --ignore-unfixed=true --vuln-type="os,library" --exit-code=1 --format="json" --output="image-trivy.json" --no-progress trivy-portainer:${{ github.sha }}
- name: Upload image security scan result as artifact
uses: actions/upload-artifact@v3
with:
name: image-security-scan-feature-result
path: image-trivy.json
- name: Download artifacts from develop branch
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: Export scan result to html file
run: |
$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="/data/image-trivy-develop.json" -output-type=table -export -export-filename="/data/image-result")
- name: Upload image result html file
uses: actions/upload-artifact@v3
with:
name: html-image-result-compare-to-develop-${{github.run_id}}
path: image-result.html
- name: Analyse the image diff result
id: set-diff-matrix
run: |
result=$(docker run --rm -v ${{ github.workspace }}:/data oscarzhou/scan-report:0.1.8 diff -report-type=trivy -path="/data/image-trivy-feature.json" -compare-to="./data/image-trivy-develop.json" -output-type=matrix)
echo "::set-output name=image_diff_result::${result}"
result-analysis:
name: Analyse scan result compared to develop
needs: [client-dependencies, server-dependencies, image-vulnerability]
runs-on: ubuntu-latest
if: >-
github.event.pull_request &&
github.event.review.body == '/scan'
strategy:
matrix:
jsdiff: ${{fromJson(needs.client-dependencies.outputs.jsdiff)}}
godiff: ${{fromJson(needs.server-dependencies.outputs.godiff)}}
imagediff: ${{fromJson(needs.image-vulnerability.outputs.imagediff)}}
steps:
- name: Check job status of diff result
if: >-
matrix.jsdiff.status == 'failure' ||
matrix.godiff.status == 'failure' ||
matrix.imagediff.status == 'failure'
run: |
echo ${{ matrix.jsdiff.status }}
echo ${{ matrix.godiff.status }}
echo ${{ matrix.imagediff.status }}
echo ${{ matrix.jsdiff.summary }}
echo ${{ matrix.godiff.summary }}
echo ${{ matrix.imagediff.summary }}
exit 1

29
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Test
on: push
jobs:
test-client:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
cache: 'yarn'
- run: yarn --frozen-lockfile
- name: Run tests
run: yarn test:client
# test-server:
# runs-on: ubuntu-latest
# env:
# GOPRIVATE: "github.com/portainer"
# steps:
# - uses: actions/checkout@v3
# - uses: actions/setup-go@v3
# with:
# go-version: '1.18'
# - name: Run tests
# run: |
# cd api
# go test ./...

View File

@@ -1 +1,2 @@
dist
dist
api/datastore/test_data

View File

@@ -4,21 +4,16 @@
"htmlWhitespaceSensitivity": "strict",
"overrides": [
{
"files": [
"*.html"
],
"files": ["*.html"],
"options": {
"parser": "angular"
}
},
{
"files": [
"*.{j,t}sx",
"*.ts"
],
"files": ["*.{j,t}sx", "*.ts"],
"options": {
"printWidth": 80
}
}
]
}
}

View File

@@ -16,6 +16,9 @@ module.exports = {
exportLocalsConvention: 'camelCaseOnly',
},
},
postcssLoaderOptions: {
implementation: require('postcss'),
},
},
},
],

View File

@@ -9,7 +9,7 @@
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/api/cmd/portainer/main.go",
"program": "${workspaceRoot}/api/cmd/portainer",
"cwd": "${workspaceRoot}",
"env": {},
"showLog": true,

View File

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

View File

@@ -42,10 +42,10 @@ View [this](https://www.portainer.io/products) table to see all of the Portainer
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
Learn more about Portainers community support channels [here.](https://www.portainer.io/community_help)
Learn more about Portainer's community support channels [here.](https://www.portainer.io/community_help)
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
- Slack (chat): [https://portainer.io/slack](https://portainer.io/slack)
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.

View File

@@ -3,32 +3,48 @@ package adminmonitor
import (
"context"
"log"
"net/http"
"strings"
"sync"
"time"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
var logFatalf = log.Fatalf
const RedirectReasonAdminInitTimeout string = "AdminInitTimeout"
type Monitor struct {
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
mu sync.Mutex
adminInitDisabled bool
}
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
// New creates a monitor that when started will wait for the timeout duration and then sends the timeout signal to disable the application
func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
adminInitDisabled: false,
}
}
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc != nil {
return
}
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
@@ -41,7 +57,11 @@ func (m *Monitor) Start() {
logFatalf("%s", err)
}
if !initialized {
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
log.Println("[INFO] [internal,init] The Portainer instance timed out for security purposes. To re-enable your Portainer instance, you will need to restart Portainer")
m.mu.Lock()
defer m.mu.Unlock()
m.adminInitDisabled = true
return
}
case <-cancellationCtx.Done():
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
@@ -53,6 +73,9 @@ func (m *Monitor) Start() {
// Stop stops monitor. Safe to call even if monitor wasn't started.
func (m *Monitor) Stop() {
m.mu.Lock()
defer m.mu.Unlock()
if m.cancellationFunc == nil {
return
}
@@ -68,3 +91,25 @@ func (m *Monitor) WasInitialized() (bool, error) {
}
return len(users) > 0, nil
}
func (m *Monitor) WasInstanceDisabled() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.adminInitDisabled
}
// WithRedirect checks whether administrator initialisation timeout. If so, it will return the error with redirect reason.
// Otherwise, it will pass through the request to next
func (m *Monitor) WithRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if m.WasInstanceDisabled() {
if strings.HasPrefix(r.RequestURI, "/api") && r.RequestURI != "/api/status" && r.RequestURI != "/api/settings/public" {
w.Header().Set("redirect-reason", RedirectReasonAdminInitTimeout)
httperror.WriteError(w, http.StatusSeeOther, "Administrator initialization timeout", nil)
return
}
}
next.ServeHTTP(w, r)
})
}

View File

@@ -21,6 +21,18 @@ func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
monitor.Stop()
}
func Test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
go monitor.Start()
monitor.Start()
go monitor.Stop()
monitor.Stop()
time.Sleep(2 * time.Second)
}
func Test_canStopStartedMonitor(t *testing.T) {
monitor := New(1*time.Minute, nil, context.Background())
monitor.Start()
@@ -30,21 +42,13 @@ func Test_canStopStartedMonitor(t *testing.T) {
assert.Nil(t, monitor.cancellationFunc, "cancellation function should absent in stopped monitor")
}
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
var fataled bool
origLogFatalf := logFatalf
logFatalf = func(s string, v ...interface{}) { fataled = true }
defer func() {
logFatalf = origLogFatalf
}()
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(2 * timeout)
assert.True(t, fataled, "monitor should been timeout and fatal")
<-time.After(20 * timeout)
assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled")
}

View File

@@ -50,4 +50,15 @@ Instead, it acts as a reverse-proxy to the Docker HTTP API. This means that you
To do so, you can use the `/endpoints/{id}/docker` Portainer API environment(endpoint) (which is not documented below due to Swagger limitations). This environment(endpoint) has a restricted access policy so you still need to be authenticated to be able to query this environment(endpoint). Any query on this environment(endpoint) will be proxied to the Docker API of the associated environment(endpoint) (requests and responses objects are the same as documented in the Docker API).
# Private Registry
Using private registry, you will need to pass a based64 encoded JSON string {"registryId":\<registryID value\>} inside the Request Header. The parameter name is "X-Registry-Auth".
\<registryID value\> - The registry ID where the repository was created.
Example:
```
eyJyZWdpc3RyeUlkIjoxfQ==
```
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://documentation.portainer.io/api/api-examples/).

View File

@@ -20,7 +20,7 @@ func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
func Test_GenerateApiKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -74,7 +74,7 @@ func Test_GenerateApiKey(t *testing.T) {
func Test_GetAPIKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -94,7 +94,7 @@ func Test_GetAPIKey(t *testing.T) {
func Test_GetAPIKeys(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -115,7 +115,7 @@ func Test_GetAPIKeys(t *testing.T) {
func Test_GetDigestUserAndKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -151,7 +151,7 @@ func Test_GetDigestUserAndKey(t *testing.T) {
func Test_UpdateAPIKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -199,7 +199,7 @@ func Test_UpdateAPIKey(t *testing.T) {
func Test_DeleteAPIKey(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -240,7 +240,7 @@ func Test_DeleteAPIKey(t *testing.T) {
func Test_InvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())

View File

@@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
@@ -65,5 +66,20 @@ func restoreFiles(srcDir string, destinationDir string) error {
return err
}
}
return nil
// TODO: This is very boltdb module specific once again due to the filename. Move to bolt module? Refactor for another day
// Prevent the possibility of having both databases. Remove any default new instance
os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName))
os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName))
// Now copy the database. It'll be either portainer.db or portainer.edb
// Note: CopyPath does not return an error if the source file doesn't exist
err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir)
if err != nil {
return err
}
return filesystem.CopyPath(filepath.Join(srcDir, boltdb.DatabaseFileName), destinationDir)
}

View File

@@ -1,14 +1,13 @@
package chisel
import (
"strconv"
portainer "github.com/portainer/portainer/api"
)
// AddEdgeJob register an EdgeJob inside the tunnel details associated to an environment(endpoint).
func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *portainer.EdgeJob) {
tunnel := service.GetTunnelDetails(endpointID)
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
existingJobIndex := -1
for idx, existingJob := range tunnel.Jobs {
@@ -24,24 +23,25 @@ func (service *Service) AddEdgeJob(endpointID portainer.EndpointID, edgeJob *por
tunnel.Jobs[existingJobIndex] = *edgeJob
}
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
service.mu.Unlock()
}
// RemoveEdgeJob will remove the specified Edge job from each tunnel it was registered with.
func (service *Service) RemoveEdgeJob(edgeJobID portainer.EdgeJobID) {
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnelDetails := item.Val.(*portainer.TunnelDetails)
service.mu.Lock()
updatedJobs := make([]portainer.EdgeJob, 0)
for _, edgeJob := range tunnelDetails.Jobs {
if edgeJob.ID == edgeJobID {
continue
for _, tunnel := range service.tunnelDetailsMap {
// Filter in-place
n := 0
for _, edgeJob := range tunnel.Jobs {
if edgeJob.ID != edgeJobID {
tunnel.Jobs[n] = edgeJob
n++
}
updatedJobs = append(updatedJobs, edgeJob)
}
tunnelDetails.Jobs = updatedJobs
service.tunnelDetailsMap.Set(item.Key, tunnelDetails)
tunnel.Jobs = tunnel.Jobs[:n]
}
service.mu.Unlock()
}

View File

@@ -3,17 +3,16 @@ package chisel
import (
"context"
"fmt"
"github.com/portainer/portainer/api/http/proxy"
"log"
"net/http"
"strconv"
"sync"
"time"
"github.com/dchest/uniuri"
chserver "github.com/jpillora/chisel/server"
cmap "github.com/orcaman/concurrent-map"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
)
const (
@@ -28,18 +27,19 @@ const (
type Service struct {
serverFingerprint string
serverPort string
tunnelDetailsMap cmap.ConcurrentMap
tunnelDetailsMap map[portainer.EndpointID]*portainer.TunnelDetails
dataStore dataservices.DataStore
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
ProxyManager *proxy.Manager
mu sync.Mutex
}
// NewService returns a pointer to a new instance of Service
func NewService(dataStore dataservices.DataStore, shutdownCtx context.Context) *Service {
return &Service{
tunnelDetailsMap: cmap.New(),
tunnelDetailsMap: make(map[portainer.EndpointID]*portainer.TunnelDetails),
dataStore: dataStore,
shutdownCtx: shutdownCtx,
}
@@ -58,11 +58,7 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
Timeout: 3 * time.Second,
}
_, err = httpClient.Do(req)
if err != nil {
return err
}
return nil
return err
}
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
@@ -185,42 +181,37 @@ func (service *Service) startTunnelVerificationLoop() {
}
func (service *Service) checkTunnels() {
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnel := item.Val.(*portainer.TunnelDetails)
tunnels := make(map[portainer.EndpointID]portainer.TunnelDetails)
service.mu.Lock()
for key, tunnel := range service.tunnelDetailsMap {
tunnels[key] = *tunnel
}
service.mu.Unlock()
for endpointID, tunnel := range tunnels {
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
continue
}
elapsed := time.Since(tunnel.LastActivity)
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [message: environment tunnel monitoring]", endpointID, tunnel.Status, elapsed.Seconds())
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
}
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
continue
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %d] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", endpointID, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
endpointID, err := strconv.Atoi(item.Key)
err := service.snapshotEnvironment(endpointID, tunnel.Port)
if err != nil {
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %d): %s", endpointID, err)
}
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
if err != nil {
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge environment (id: %s): %s", item.Key, err)
}
}
endpointID, err := strconv.Atoi(item.Key)
if err != nil {
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
continue
}
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))

View File

@@ -4,13 +4,11 @@ import (
"encoding/base64"
"fmt"
"math/rand"
"strconv"
"strings"
"time"
"github.com/portainer/libcrypto"
"github.com/dchest/uniuri"
"github.com/portainer/libcrypto"
portainer "github.com/portainer/portainer/api"
)
@@ -19,13 +17,13 @@ const (
maxAvailablePort = 65535
)
// NOTE: it needs to be called with the lock acquired
// getUnusedPort is used to generate an unused random port in the dynamic port range.
// Dynamic ports (also called private ports) are 49152 to 65535.
func (service *Service) getUnusedPort() int {
port := randomInt(minAvailablePort, maxAvailablePort)
for item := range service.tunnelDetailsMap.IterBuffered() {
tunnel := item.Val.(*portainer.TunnelDetails)
for _, tunnel := range service.tunnelDetailsMap {
if tunnel.Port == port {
return service.getUnusedPort()
}
@@ -38,26 +36,32 @@ func randomInt(min, max int) int {
return min + rand.Intn(max-min)
}
// NOTE: it needs to be called with the lock acquired
func (service *Service) getTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
if tunnel, ok := service.tunnelDetailsMap[endpointID]; ok {
return tunnel
}
tunnel := &portainer.TunnelDetails{
Status: portainer.EdgeAgentIdle,
}
service.tunnelDetailsMap[endpointID] = tunnel
return tunnel
}
// GetTunnelDetails returns information about the tunnel associated to an environment(endpoint).
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
key := strconv.Itoa(int(endpointID))
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) portainer.TunnelDetails {
service.mu.Lock()
defer service.mu.Unlock()
if item, ok := service.tunnelDetailsMap.Get(key); ok {
tunnelDetails := item.(*portainer.TunnelDetails)
return tunnelDetails
}
jobs := make([]portainer.EdgeJob, 0)
return &portainer.TunnelDetails{
Status: portainer.EdgeAgentIdle,
Port: 0,
Jobs: jobs,
Credentials: "",
}
return *service.getTunnelDetails(endpointID)
}
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (portainer.TunnelDetails, error) {
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentActive {
@@ -68,13 +72,13 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
return portainer.TunnelDetails{}, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := service.dataStore.Settings().Settings()
if err != nil {
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
return portainer.TunnelDetails{}, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
@@ -83,29 +87,27 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
}
tunnel = service.GetTunnelDetails(endpoint.ID)
return tunnel, nil
return service.GetTunnelDetails(endpoint.ID), nil
}
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to ACTIVE.
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
tunnel := service.GetTunnelDetails(endpointID)
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentActive
tunnel.Credentials = ""
tunnel.LastActivity = time.Now()
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
service.mu.Unlock()
}
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to IDLE.
// It removes any existing credentials associated to the tunnel.
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
tunnel := service.GetTunnelDetails(endpointID)
service.mu.Lock()
tunnel := service.getTunnelDetails(endpointID)
tunnel.Status = portainer.EdgeAgentIdle
tunnel.Port = 0
tunnel.LastActivity = time.Now()
@@ -116,10 +118,9 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
}
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
service.ProxyManager.DeleteEndpointProxy(endpointID)
service.mu.Unlock()
}
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).
@@ -128,7 +129,10 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
// Credentials are encrypted using the Edge ID associated to the environment(endpoint).
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
tunnel := service.GetTunnelDetails(endpointID)
tunnel := service.getTunnelDetails(endpointID)
service.mu.Lock()
defer service.mu.Unlock()
if tunnel.Port == 0 {
endpoint, err := service.dataStore.Endpoint().Endpoint(endpointID)
@@ -152,9 +156,6 @@ func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointI
return err
}
tunnel.Credentials = credentials
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
}
return nil

View File

@@ -50,13 +50,17 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
BaseURL: kingpin.Flag("base-url", "Base URL parameter such as portainer if running portainer as http://yourdomain.com/portainer/.").Short('b').Default(defaultBaseURL).String(),
InitialMmapSize: kingpin.Flag("initial-mmap-size", "Initial mmap size of the database in bytes").Int(),
MaxBatchSize: kingpin.Flag("max-batch-size", "Maximum size of a batch").Int(),
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
}
kingpin.Parse()
@@ -125,7 +129,7 @@ func validateEndpointURL(endpointURL string) error {
}
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval != defaultSnapshotInterval {
if snapshotInterval != "" {
_, err := time.ParseDuration(snapshotInterval)
if err != nil {
return errInvalidSnapshotInterval

View File

@@ -18,8 +18,6 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"
)

View File

@@ -15,8 +15,7 @@ const (
defaultHTTPDisabled = "false"
defaultHTTPEnabled = "false"
defaultSSL = "false"
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
defaultSecretKeyName = "portainer"
)

View File

@@ -13,7 +13,7 @@ func importFromJson(fileService portainer.FileService, store *datastore.Store) {
importFile := "/data/import.json"
if exists, _ := fileService.FileExists(importFile); exists {
if err := store.Import(importFile); err != nil {
logrus.WithError(err).Debugf("import %s failed", importFile)
logrus.WithError(err).Debugf("Import %s failed", importFile)
// TODO: should really rollback on failure, but then we have nothing.
} else {
@@ -23,7 +23,7 @@ func importFromJson(fileService portainer.FileService, store *datastore.Store) {
// I also suspect that everything from "Init to Init" is potentially a migration
err := store.Init()
if err != nil {
log.Fatalf("failed initializing data store: %v", err)
log.Fatalf("Failed initializing data store: %v", err)
}
}
}

View File

@@ -10,7 +10,8 @@ func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
formatter := &logrus.TextFormatter{DisableTimestamp: false, DisableLevelTruncation: true}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatter)

View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"crypto/sha256"
"fmt"
"log"
"os"
@@ -19,6 +20,7 @@ import (
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/database"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/docker"
@@ -47,12 +49,12 @@ func initCLI() *portainer.CLIFlags {
var cliService portainer.CLIService = &cli.Service{}
flags, err := cliService.ParseFlags(portainer.APIVersion)
if err != nil {
log.Fatalf("failed parsing flags: %v", err)
logrus.Fatalf("Failed parsing flags: %v", err)
}
err = cliService.ValidateFlags(flags)
if err != nil {
log.Fatalf("failed validating flags:%v", err)
logrus.Fatalf("Failed validating flags:%v", err)
}
return flags
}
@@ -60,62 +62,72 @@ func initCLI() *portainer.CLIFlags {
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatalf("failed creating file service: %v", err)
logrus.Fatalf("Failed creating file service: %v", err)
}
return fileService
}
func initDataStore(flags *portainer.CLIFlags, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
connection, err := database.NewDatabase("boltdb", *flags.Data)
func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService portainer.FileService, shutdownCtx context.Context) dataservices.DataStore {
connection, err := database.NewDatabase("boltdb", *flags.Data, secretKey)
if err != nil {
panic(err)
logrus.Fatalf("failed creating database connection: %s", err)
}
if bconn, ok := connection.(*boltdb.DbConnection); ok {
bconn.MaxBatchSize = *flags.MaxBatchSize
bconn.MaxBatchDelay = *flags.MaxBatchDelay
bconn.InitialMmapSize = *flags.InitialMmapSize
} else {
logrus.Fatalf("failed creating database connection: expecting a boltdb database type but a different one was received")
}
store := datastore.NewStore(*flags.Data, fileService, connection)
isNew, err := store.Open()
if err != nil {
log.Fatalf("failed opening store: %v", err)
logrus.Fatalf("Failed opening store: %v", err)
}
if *flags.Rollback {
err := store.Rollback(false)
if err != nil {
log.Fatalf("failed rolling back: %s", err)
logrus.Fatalf("Failed rolling back: %v", err)
}
log.Println("Exiting rollback")
logrus.Println("Exiting rollback")
os.Exit(0)
return nil
}
// Init sets some defaults - its basically a migration
// Init sets some defaults - it's basically a migration
err = store.Init()
if err != nil {
log.Fatalf("failed initializing data store: %v", err)
logrus.Fatalf("Failed initializing data store: %v", err)
}
if isNew {
// from MigrateData
store.VersionService.StoreDBVersion(portainer.DBVersion)
// Disabled for now. Can't use feature flags due to the way that works
// EXPERIMENTAL, will only activate if `/data/import.json` exists
//importFromJson(fileService, store)
err := updateSettingsFromFlags(store, flags)
if err != nil {
log.Fatalf("failed updating settings from flags: %v", err)
logrus.Fatalf("Failed updating settings from flags: %v", err)
}
} else {
storedVersion, err := store.VersionService.DBVersion()
if err != nil {
logrus.Fatalf("Something Failed during creation of new database: %v", err)
}
if storedVersion != portainer.DBVersion {
err = store.MigrateData()
if err != nil {
logrus.Fatalf("Failed migration: %v", err)
}
}
}
storedVersion, err := store.VersionService.DBVersion()
err = updateSettingsFromFlags(store, flags)
if err != nil {
log.Fatalf("Something failed during creation of new database: %v", err)
}
if storedVersion != portainer.DBVersion {
err = store.MigrateData()
if err != nil {
log.Fatalf("failed migration: %v", err)
}
log.Fatalf("Failed updating settings from flags: %v", err)
}
// this is for the db restore functionality - needs more tests.
@@ -127,7 +139,7 @@ func initDataStore(flags *portainer.CLIFlags, fileService portainer.FileService,
err := store.Export(exportFilename)
if err != nil {
logrus.WithError(err).Debugf("failed to export to %s", exportFilename)
logrus.WithError(err).Debugf("Failed to export to %s", exportFilename)
} else {
logrus.Debugf("exported to %s", exportFilename)
}
@@ -139,7 +151,7 @@ func initDataStore(flags *portainer.CLIFlags, fileService portainer.FileService,
func initComposeStackManager(assetsPath string, configPath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
composeWrapper, err := exec.NewComposeStackManager(assetsPath, configPath, proxyManager)
if err != nil {
log.Fatalf("failed creating compose manager: %s", err)
logrus.Fatalf("Failed creating compose manager: %v", err)
}
return composeWrapper
@@ -196,7 +208,7 @@ func initGitService() portainer.GitService {
return git.NewService()
}
func initSSLService(addr, dataPath, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
func initSSLService(addr, certPath, keyPath string, fileService portainer.FileService, dataStore dataservices.DataStore, shutdownTrigger context.CancelFunc) (*ssl.Service, error) {
slices := strings.Split(addr, ":")
host := slices[0]
if host == "" {
@@ -221,11 +233,11 @@ func initKubernetesClientFactory(signatureService portainer.DigitalSignatureServ
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
}
func initSnapshotService(snapshotInterval string, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
func initSnapshotService(snapshotIntervalFromFlag string, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory, shutdownCtx context.Context) (portainer.SnapshotService, error) {
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
snapshotService, err := snapshot.NewService(snapshotInterval, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
snapshotService, err := snapshot.NewService(snapshotIntervalFromFlag, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx)
if err != nil {
return nil, err
}
@@ -246,11 +258,17 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
return err
}
settings.LogoURL = *flags.Logo
settings.SnapshotInterval = *flags.SnapshotInterval
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
settings.EnableTelemetry = true
settings.OAuthSettings.SSO = true
if *flags.SnapshotInterval != "" {
settings.SnapshotInterval = *flags.SnapshotInterval
}
if *flags.Logo != "" {
settings.LogoURL = *flags.Logo
}
if *flags.EnableEdgeComputeFeatures {
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
}
if *flags.Templates != "" {
settings.TemplatesURL = *flags.Templates
@@ -260,6 +278,12 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
settings.BlackListedLabels = *flags.Labels
}
if agentKey, ok := os.LookupEnv("AGENT_SECRET"); ok {
settings.AgentSecret = agentKey
} else {
settings.AgentSecret = ""
}
err = dataStore.Settings().UpdateSettings(settings)
if err != nil {
return err
@@ -272,8 +296,8 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
if *flags.HTTPDisabled {
sslSettings.HTTPEnabled = false
} else {
sslSettings.HTTPEnabled = *flags.HTTPEnabled || sslSettings.HTTPEnabled
} else if *flags.HTTPEnabled {
sslSettings.HTTPEnabled = true
}
err = dataStore.SSLSettings().UpdateSettings(sslSettings)
@@ -316,9 +340,9 @@ func enableFeaturesFromFlags(dataStore dataservices.DataStore, flags *portainer.
}
if featureState {
log.Printf("Feature %v : on", *correspondingFeature)
logrus.Printf("Feature %v : on", *correspondingFeature)
} else {
log.Printf("Feature %v : off", *correspondingFeature)
logrus.Printf("Feature %v : off", *correspondingFeature)
}
settings.FeatureFlagSettings[*correspondingFeature] = featureState
@@ -347,7 +371,7 @@ func generateAndStoreKeyPair(fileService portainer.FileService, signatureService
func initKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
existingKeyPair, err := fileService.KeyPairFilesExist()
if err != nil {
log.Fatalf("failed checking for existing key pair: %v", err)
logrus.Fatalf("Failed checking for existing key pair: %v", err)
}
if existingKeyPair {
@@ -380,7 +404,6 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.
TLSConfig: tlsConfiguration,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
@@ -418,7 +441,7 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore dataservices.
err := snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
logrus.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
return dataStore.Endpoint().Create(endpoint)
@@ -442,7 +465,6 @@ func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStor
TLSConfig: portainer.TLSConfiguration{},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
@@ -464,7 +486,7 @@ func createUnsecuredEndpoint(endpointURL string, dataStore dataservices.DataStor
err := snapshotService.SnapshotEndpoint(endpoint)
if err != nil {
log.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
logrus.Printf("http error: environment snapshot error (environment=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
}
return dataStore.Endpoint().Create(endpoint)
@@ -481,7 +503,7 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, s
}
if len(endpoints) > 0 {
log.Println("Instance already has defined environments. Skipping the environment defined via CLI.")
logrus.Println("Instance already has defined environments. Skipping the environment defined via CLI.")
return nil
}
@@ -491,59 +513,80 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore dataservices.DataStore, s
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
}
func loadEncryptionSecretKey(keyfilename string) []byte {
content, err := os.ReadFile(path.Join("/run/secrets", keyfilename))
if err != nil {
if os.IsNotExist(err) {
logrus.Printf("Encryption key file `%s` not present", keyfilename)
} else {
logrus.Printf("Error reading encryption key file: %v", err)
}
return nil
}
// return a 32 byte hash of the secret (required for AES)
hash := sha256.Sum256(content)
return hash[:]
}
func buildServer(flags *portainer.CLIFlags) portainer.Server {
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
fileService := initFileService(*flags.Data)
encryptionKey := loadEncryptionSecretKey(*flags.SecretKeyName)
if encryptionKey == nil {
logrus.Println("Proceeding without encryption key")
}
dataStore := initDataStore(flags, fileService, shutdownCtx)
dataStore := initDataStore(flags, encryptionKey, fileService, shutdownCtx)
if err := dataStore.CheckCurrentEdition(); err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
instanceID, err := dataStore.Version().InstanceID()
if err != nil {
log.Fatalf("failed getting instance id: %v", err)
logrus.Fatalf("Failed getting instance id: %v", err)
}
apiKeyService := initAPIKeyService(dataStore)
settings, err := dataStore.Settings().Settings()
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
jwtService, err := initJWTService(settings.UserSessionTimeout, dataStore)
if err != nil {
log.Fatalf("failed initializing JWT service: %v", err)
logrus.Fatalf("Failed initializing JWT service: %v", err)
}
err = enableFeaturesFromFlags(dataStore, flags)
if err != nil {
log.Fatalf("failed enabling feature flag: %v", err)
logrus.Fatalf("Failed enabling feature flag: %v", err)
}
ldapService := initLDAPService()
oauthService := initOAuthService()
gitService := initGitService()
openAMTService := openamt.NewService(dataStore)
openAMTService := openamt.NewService()
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.Data, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
sslService, err := initSSLService(*flags.AddrHTTPS, *flags.SSLCert, *flags.SSLKey, fileService, dataStore, shutdownTrigger)
if err != nil {
log.Fatal(err)
logrus.Fatal(err)
}
sslSettings, err := sslService.GetSSLSettings()
if err != nil {
log.Fatalf("failed to get ssl settings: %s", err)
logrus.Fatalf("Failed to get ssl settings: %s", err)
}
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
log.Fatalf("failed initializing key pair: %v", err)
logrus.Fatalf("Failed initializing key pair: %v", err)
}
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
@@ -553,7 +596,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil {
log.Fatalf("failed initializing snapshot service: %v", err)
logrus.Fatalf("Failed initializing snapshot service: %v", err)
}
snapshotService.Start()
@@ -562,7 +605,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
kubeConfigService := kubernetes.NewKubeConfigCAService(*flags.AddrHTTPS, sslSettings.CertPath)
kubeClusterAccessService := kubernetes.NewKubeClusterAccessService(*flags.BaseURL, *flags.AddrHTTPS, sslSettings.CertPath)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
@@ -574,37 +617,37 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
log.Fatalf("failed initializing swarm stack manager: %s", err)
logrus.Fatalf("Failed initializing swarm stack manager: %v", err)
}
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
if err != nil {
log.Fatalf("failed initializing helm package manager: %s", err)
logrus.Fatalf("Failed initializing helm package manager: %v", err)
}
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil {
log.Fatalf("failed loading edge jobs from database: %v", err)
logrus.Fatalf("Failed loading edge jobs from database: %v", err)
}
applicationStatus := initStatus(instanceID)
err = initEndpoint(flags, dataStore, snapshotService)
if err != nil {
log.Fatalf("failed initializing environment: %v", err)
logrus.Fatalf("Failed initializing environment: %v", err)
}
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
if err != nil {
log.Fatalf("failed getting admin password file: %v", err)
logrus.Fatalf("Failed getting admin password file: %v", err)
}
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
if err != nil {
log.Fatalf("failed hashing admin password: %v", err)
logrus.Fatalf("Failed hashing admin password: %v", err)
}
} else if *flags.AdminPassword != "" {
adminPasswordHash = *flags.AdminPassword
@@ -613,11 +656,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
if adminPasswordHash != "" {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {
log.Fatalf("failed getting admin user: %v", err)
logrus.Fatalf("Failed getting admin user: %v", err)
}
if len(users) == 0 {
log.Println("Created admin user with the given password.")
logrus.Println("Created admin user with the given password.")
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
@@ -625,21 +668,21 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
err := dataStore.User().Create(user)
if err != nil {
log.Fatalf("failed creating admin user: %v", err)
logrus.Fatalf("Failed creating admin user: %v", err)
}
} else {
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
logrus.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
}
}
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
log.Fatalf("failed starting tunnel server: %s", err)
logrus.Fatalf("Failed starting tunnel server: %v", err)
}
sslDBSettings, err := dataStore.SSLSettings().Settings()
if err != nil {
log.Fatalf("failed to fetch ssl settings from DB")
logrus.Fatalf("Failed to fetch ssl settings from DB")
}
scheduler := scheduler.NewScheduler(shutdownCtx)
@@ -669,7 +712,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
OpenAMTService: openAMTService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeConfigService: kubeConfigService,
KubeClusterAccessService: kubeClusterAccessService,
SignatureService: digitalSignatureService,
SnapshotService: snapshotService,
SSLService: sslService,
@@ -679,7 +722,6 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
BaseURL: *flags.BaseURL,
}
}
@@ -690,8 +732,8 @@ func main() {
for {
server := buildServer(flags)
log.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
err := server.Start()
log.Printf("[INFO] [cmd,main] Http server exited: %s\n", err)
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
}
}

View File

@@ -21,7 +21,7 @@ func (m mockKingpinSetting) SetValue(value kingpin.Value) {
func Test_enableFeaturesFromFlags(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
tests := []struct {
@@ -29,11 +29,6 @@ func Test_enableFeaturesFromFlags(t *testing.T) {
isSupported bool
}{
{"test", false},
{"openamt", false},
{"open-amt", true},
{"oPeN-amT", true},
{"fdo", true},
{"FDO", true},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
@@ -81,7 +76,7 @@ func Test_optionalFeature(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
// Enable the test feature

View File

@@ -11,14 +11,16 @@ type Connection interface {
// write the db contents to filename as json (the schema needs defining)
ExportRaw(filename string) error
//Rollback(force bool) error
//MigrateData(migratorParams *database.MigratorParameters, force bool) error
// TODO: this one is very database specific atm
BackupTo(w io.Writer) error
GetDatabaseFilename() string
GetDatabaseFileName() string
GetDatabaseFilePath() string
GetStorePath() string
IsEncryptedStore() bool
NeedsEncryptionMigration() (bool, error)
SetEncrypted(encrypted bool)
SetServiceName(bucketName string) error
GetObject(bucketName string, key []byte, object interface{}) error
UpdateObject(bucketName string, key []byte, object interface{}) error
@@ -27,8 +29,12 @@ type Connection interface {
GetNextIdentifier(bucketName string) int
CreateObject(bucketName string, fn func(uint64) (int, interface{})) error
CreateObjectWithId(bucketName string, id int, obj interface{}) error
CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error
CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error
GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
GetAllWithJsoniter(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error
ConvertToKey(v int) []byte
BackupMetadata() (map[string]interface{}, error)
RestoreMetadata(s map[string]interface{}) error
}

View File

@@ -9,11 +9,11 @@ type Service struct{}
// Hash hashes a string using the bcrypt algorithm
func (*Service) Hash(data string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
bytes, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
if err != nil {
return "", nil
return "", err
}
return string(hash), nil
return string(bytes), err
}
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.

53
api/crypto/hash_test.go Normal file
View File

@@ -0,0 +1,53 @@
package crypto
import (
"testing"
)
func TestService_Hash(t *testing.T) {
var s = &Service{}
type args struct {
hash string
data string
}
tests := []struct {
name string
args args
expect bool
}{
{
name: "Empty",
args: args{
hash: "",
data: "",
},
expect: false,
},
{
name: "Matching",
args: args{
hash: "$2a$10$6BFGd94oYx8k0bFNO6f33uPUpcpAJyg8UVX.akLe9EthF/ZBTXqcy",
data: "Passw0rd!",
},
expect: true,
},
{
name: "Not matching",
args: args{
hash: "$2a$10$ltKrUZ7492xyutHOb0/XweevU4jyw7QO66rP32jTVOMb3EX3JxA/a",
data: "Passw0rd!",
},
expect: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := s.CompareHashAndData(tt.args.hash, tt.args.data)
if (err != nil) == tt.expect {
t.Errorf("Service.CompareHashAndData() = %v", err)
}
})
}
}

View File

@@ -2,6 +2,7 @@ package boltdb
import (
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -9,45 +10,129 @@ import (
"path"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api/dataservices/errors"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
)
const (
DatabaseFileName = "portainer.db"
DatabaseFileName = "portainer.db"
EncryptedDatabaseFileName = "portainer.edb"
)
var (
ErrHaveEncryptedAndUnencrypted = errors.New("Portainer has detected both an encrypted and un-encrypted database and cannot start. Only one database should exist")
ErrHaveEncryptedWithNoKey = errors.New("The portainer database is encrypted, but no secret was loaded")
)
type DbConnection struct {
Path string
Path string
MaxBatchSize int
MaxBatchDelay time.Duration
InitialMmapSize int
EncryptionKey []byte
isEncrypted bool
*bolt.DB
}
func (connection *DbConnection) GetDatabaseFilename() string {
// GetDatabaseFileName get the database filename
func (connection *DbConnection) GetDatabaseFileName() string {
if connection.IsEncryptedStore() {
return EncryptedDatabaseFileName
}
return DatabaseFileName
}
// GetDataseFilePath get the path + filename for the database file
func (connection *DbConnection) GetDatabaseFilePath() string {
if connection.IsEncryptedStore() {
return path.Join(connection.Path, EncryptedDatabaseFileName)
}
return path.Join(connection.Path, DatabaseFileName)
}
// GetStorePath get the filename and path for the database file
func (connection *DbConnection) GetStorePath() string {
return connection.Path
}
func (connection *DbConnection) SetEncrypted(flag bool) {
connection.isEncrypted = flag
}
// Return true if the database is encrypted
func (connection *DbConnection) IsEncryptedStore() bool {
return connection.getEncryptionKey() != nil
}
// NeedsEncryptionMigration returns true if database encryption is enabled and
// we have an un-encrypted DB that requires migration to an encrypted DB
func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
// 1) portainer.edb + key => False
// 2) portainer.edb + no key => ERROR Fatal!
// 3) portainer.db + key => True (needs migration)
// 4) portainer.db + no key => False
// 5) NoDB (new) + key => False
// 6) NoDB (new) + no key => False
// 7) portainer.db & portainer.edb => ERROR Fatal!
// If we have a loaded encryption key, always set encrypted
if connection.EncryptionKey != nil {
connection.SetEncrypted(true)
}
// Check for portainer.db
dbFile := path.Join(connection.Path, DatabaseFileName)
_, err := os.Stat(dbFile)
haveDbFile := err == nil
// Check for portainer.edb
edbFile := path.Join(connection.Path, EncryptedDatabaseFileName)
_, err = os.Stat(edbFile)
haveEdbFile := err == nil
if haveDbFile && haveEdbFile {
// 7 - encrypted and unencrypted db?
return false, ErrHaveEncryptedAndUnencrypted
}
if haveDbFile && connection.EncryptionKey != nil {
// 3 - needs migration
return true, nil
}
if haveEdbFile && connection.EncryptionKey == nil {
// 2 - encrypted db, but no key?
return false, ErrHaveEncryptedWithNoKey
}
// 1, 4, 5, 6
return false, nil
}
// Open opens and initializes the BoltDB database.
func (connection *DbConnection) Open() error {
// Disabled for now. Can't use feature flags due to the way that works
// databaseExportPath := path.Join(connection.Path, fmt.Sprintf("raw-%s-%d.json", DatabaseFileName, time.Now().Unix()))
// if err := connection.ExportRaw(databaseExportPath); err != nil {
// log.Printf("raw export to %s error: %s", databaseExportPath, err)
// } else {
// log.Printf("raw export to %s success", databaseExportPath)
// }
logrus.Infof("Loading PortainerDB: %s", connection.GetDatabaseFileName())
databasePath := path.Join(connection.Path, DatabaseFileName)
db, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second})
// Now we open the db
databasePath := connection.GetDatabaseFilePath()
db, err := bolt.Open(databasePath, 0600, &bolt.Options{
Timeout: 1 * time.Second,
InitialMmapSize: connection.InitialMmapSize,
})
if err != nil {
return err
}
db.MaxBatchSize = connection.MaxBatchSize
db.MaxBatchDelay = connection.MaxBatchDelay
connection.DB = db
return nil
}
@@ -71,12 +156,12 @@ func (connection *DbConnection) BackupTo(w io.Writer) error {
}
func (connection *DbConnection) ExportRaw(filename string) error {
databasePath := path.Join(connection.Path, DatabaseFileName)
databasePath := connection.GetDatabaseFilePath()
if _, err := os.Stat(databasePath); err != nil {
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
}
b, err := exportJson(databasePath)
b, err := connection.ExportJson(databasePath, true)
if err != nil {
return err
}
@@ -94,12 +179,9 @@ func (connection *DbConnection) ConvertToKey(v int) []byte {
// CreateBucket is a generic function used to create a bucket inside a database database.
func (connection *DbConnection) SetServiceName(bucketName string) error {
return connection.Update(func(tx *bolt.Tx) error {
return connection.Batch(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return nil
return err
})
}
@@ -112,7 +194,7 @@ func (connection *DbConnection) GetObject(bucketName string, key []byte, object
value := bucket.Get(key)
if value == nil {
return errors.ErrObjectNotFound
return dserrors.ErrObjectNotFound
}
data = make([]byte, len(value))
@@ -124,31 +206,33 @@ func (connection *DbConnection) GetObject(bucketName string, key []byte, object
return err
}
return UnmarshalObject(data, object)
return connection.UnmarshalObjectWithJsoniter(data, object)
}
func (connection *DbConnection) getEncryptionKey() []byte {
if !connection.isEncrypted {
return nil
}
return connection.EncryptionKey
}
// UpdateObject is a generic function used to update an object inside a database database.
func (connection *DbConnection) UpdateObject(bucketName string, key []byte, object interface{}) error {
return connection.Update(func(tx *bolt.Tx) error {
data, err := connection.MarshalObject(object)
if err != nil {
return err
}
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := MarshalObject(object)
if err != nil {
return err
}
err = bucket.Put(key, data)
if err != nil {
return err
}
return nil
return bucket.Put(key, data)
})
}
// DeleteObject is a generic function used to delete an object inside a database database.
func (connection *DbConnection) DeleteObject(bucketName string, key []byte) error {
return connection.Update(func(tx *bolt.Tx) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
return bucket.Delete(key)
})
@@ -157,13 +241,13 @@ func (connection *DbConnection) DeleteObject(bucketName string, key []byte) erro
// DeleteAllObjects delete all objects where matching() returns (id, ok).
// TODO: think about how to return the error inside (maybe change ok to type err, and use "notfound"?
func (connection *DbConnection) DeleteAllObjects(bucketName string, matching func(o interface{}) (id int, ok bool)) error {
return connection.Update(func(tx *bolt.Tx) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var obj interface{}
err := UnmarshalObject(v, &obj)
err := connection.UnmarshalObject(v, &obj)
if err != nil {
return err
}
@@ -184,7 +268,7 @@ func (connection *DbConnection) DeleteAllObjects(bucketName string, matching fun
func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
var identifier int
connection.Update(func(tx *bolt.Tx) error {
connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
id, err := bucket.NextSequence()
if err != nil {
@@ -199,13 +283,13 @@ func (connection *DbConnection) GetNextIdentifier(bucketName string) int {
// CreateObject creates a new object in the bucket, using the next bucket sequence id
func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64) (int, interface{})) error {
return connection.Update(func(tx *bolt.Tx) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
seqId, _ := bucket.NextSequence()
id, obj := fn(seqId)
data, err := MarshalObject(obj)
data, err := connection.MarshalObject(obj)
if err != nil {
return err
}
@@ -216,10 +300,9 @@ func (connection *DbConnection) CreateObject(bucketName string, fn func(uint64)
// CreateObjectWithId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, obj interface{}) error {
return connection.Update(func(tx *bolt.Tx) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := MarshalObject(obj)
data, err := connection.MarshalObject(obj)
if err != nil {
return err
}
@@ -228,10 +311,23 @@ func (connection *DbConnection) CreateObjectWithId(bucketName string, id int, ob
})
}
// CreateObjectWithStringId creates a new object in the bucket, using the specified id
func (connection *DbConnection) CreateObjectWithStringId(bucketName string, id []byte, obj interface{}) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
data, err := connection.MarshalObject(obj)
if err != nil {
return err
}
return bucket.Put(id, data)
})
}
// CreateObjectWithSetSequence creates a new object in the bucket, using the specified id, and sets the bucket sequence
// avoid this :)
func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, id int, obj interface{}) error {
return connection.Update(func(tx *bolt.Tx) error {
return connection.Batch(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
// We manually manage sequences for schedules
@@ -240,7 +336,7 @@ func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, i
return err
}
data, err := MarshalObject(obj)
data, err := connection.MarshalObject(obj)
if err != nil {
return err
}
@@ -252,10 +348,9 @@ func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, i
func (connection *DbConnection) GetAll(bucketName string, obj interface{}, append func(o interface{}) (interface{}, error)) error {
err := connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(bucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := UnmarshalObject(v, obj)
err := connection.UnmarshalObject(v, obj)
if err != nil {
return err
}
@@ -277,7 +372,7 @@ func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interf
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
err := UnmarshalObjectWithJsoniter(v, obj)
err := connection.UnmarshalObjectWithJsoniter(v, obj)
if err != nil {
return err
}
@@ -291,3 +386,43 @@ func (connection *DbConnection) GetAllWithJsoniter(bucketName string, obj interf
})
return err
}
func (connection *DbConnection) BackupMetadata() (map[string]interface{}, error) {
buckets := map[string]interface{}{}
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
bucket = tx.Bucket([]byte(bucketName))
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil
})
return err
})
return buckets, err
}
func (connection *DbConnection) RestoreMetadata(s map[string]interface{}) error {
var err error
for bucketName, v := range s {
id, ok := v.(float64) // JSON ints are unmarshalled to interface as float64. See: https://pkg.go.dev/encoding/json#Decoder.Decode
if !ok {
logrus.Errorf("Failed to restore metadata to bucket %s, skipped", bucketName)
continue
}
err = connection.Batch(func(tx *bolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists([]byte(bucketName))
if err != nil {
return err
}
return bucket.SetSequence(uint64(id))
})
}
return err
}

View File

@@ -0,0 +1,124 @@
package boltdb
import (
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_NeedsEncryptionMigration(t *testing.T) {
// Test the specific scenarios mentioned in NeedsEncryptionMigration
// i.e.
// Cases: Note, we need to check both portainer.db and portainer.edb
// to determine if it's a new store. We only need to differentiate between cases 2,3 and 5
// 1) portainer.edb + key => False
// 2) portainer.edb + no key => ERROR Fatal!
// 3) portainer.db + key => True (needs migration)
// 4) portainer.db + no key => False
// 5) NoDB (new) + key => False
// 6) NoDB (new) + no key => False
// 7) portainer.db & portainer.edb (key not important) => ERROR Fatal!
is := assert.New(t)
dir := t.TempDir()
cases := []struct {
name string
dbname string
key bool
expectError error
expectResult bool
}{
{
name: "portainer.edb + key",
dbname: EncryptedDatabaseFileName,
key: true,
expectError: nil,
expectResult: false,
},
{
name: "portainer.db + key (migration needed)",
dbname: DatabaseFileName,
key: true,
expectError: nil,
expectResult: true,
},
{
name: "portainer.db + no key",
dbname: DatabaseFileName,
key: false,
expectError: nil,
expectResult: false,
},
{
name: "NoDB (new) + key",
dbname: "",
key: false,
expectError: nil,
expectResult: false,
},
{
name: "NoDB (new) + no key",
dbname: "",
key: false,
expectError: nil,
expectResult: false,
},
// error tests
{
name: "portainer.edb + no key",
dbname: EncryptedDatabaseFileName,
key: false,
expectError: ErrHaveEncryptedWithNoKey,
expectResult: false,
},
{
name: "portainer.db & portainer.edb",
dbname: "both",
key: true,
expectError: ErrHaveEncryptedAndUnencrypted,
expectResult: false,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
connection := DbConnection{Path: dir}
if tc.dbname == "both" {
// Special case. If portainer.db and portainer.edb exist.
dbFile1 := path.Join(connection.Path, DatabaseFileName)
f, _ := os.Create(dbFile1)
f.Close()
defer os.Remove(dbFile1)
dbFile2 := path.Join(connection.Path, EncryptedDatabaseFileName)
f, _ = os.Create(dbFile2)
f.Close()
defer os.Remove(dbFile2)
} else if tc.dbname != "" {
dbFile := path.Join(connection.Path, tc.dbname)
f, _ := os.Create(dbFile)
f.Close()
defer os.Remove(dbFile)
}
if tc.key {
connection.EncryptionKey = []byte("secret")
}
result, err := connection.NeedsEncryptionMigration()
is.Equal(tc.expectError, err, "Fatal Error failure. Test: %s", tc.name)
is.Equal(result, tc.expectResult, "Failed test: %s", tc.name)
})
}
}

View File

@@ -4,14 +4,36 @@ import (
"encoding/json"
"time"
"github.com/boltdb/bolt"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
)
func backupMetadata(connection *bolt.DB) (map[string]interface{}, error) {
buckets := map[string]interface{}{}
err := connection.View(func(tx *bolt.Tx) error {
err := tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
bucketName := string(name)
bucket = tx.Bucket([]byte(bucketName))
seqId := bucket.Sequence()
buckets[bucketName] = int(seqId)
return nil
})
return err
})
return buckets, err
}
// ExportJSON creates a JSON representation from a DbConnection. You can include
// the database's metadata or ignore it. Ensure the database is closed before
// using this function
// inspired by github.com/konoui/boltdb-exporter (which has no license)
// but very much simplified, based on how we use boltdb
func (c *DbConnection) ExportJson(databasePath string, metadata bool) ([]byte, error) {
logrus.WithField("databasePath", databasePath).Infof("exportJson")
func exportJson(databasePath string) ([]byte, error) {
connection, err := bolt.Open(databasePath, 0600, &bolt.Options{Timeout: 1 * time.Second, ReadOnly: true})
if err != nil {
return []byte("{}"), err
@@ -19,6 +41,13 @@ func exportJson(databasePath string) ([]byte, error) {
defer connection.Close()
backup := make(map[string]interface{})
if metadata {
meta, err := backupMetadata(connection)
if err != nil {
logrus.WithError(err).Errorf("Failed exporting metadata: %v", err)
}
backup["__metadata"] = meta
}
err = connection.View(func(tx *bolt.Tx) error {
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
@@ -31,7 +60,7 @@ func exportJson(databasePath string) ([]byte, error) {
continue
}
var obj interface{}
err := UnmarshalObject(v, &obj)
err := c.UnmarshalObject(v, &obj)
if err != nil {
logrus.WithError(err).Errorf("Failed to unmarshal (bucket %s): %v", bucketName, string(v))
obj = v
@@ -44,15 +73,20 @@ func exportJson(databasePath string) ([]byte, error) {
}
if bucketName == "version" {
backup[bucketName] = version
return nil
}
if len(list) > 0 {
if bucketName == "ssl" ||
bucketName == "settings" ||
bucketName == "tunnel_server" {
backup[bucketName] = list[0]
backup[bucketName] = nil
if len(list) > 0 {
backup[bucketName] = list[0]
}
return nil
}
backup[bucketName] = list
return nil
}
return nil

View File

@@ -1,26 +1,72 @@
package boltdb
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"fmt"
"io"
jsoniter "github.com/json-iterator/go"
"github.com/pkg/errors"
)
var errEncryptedStringTooShort = fmt.Errorf("encrypted string too short")
// MarshalObject encodes an object to binary format
func MarshalObject(object interface{}) ([]byte, error) {
func (connection *DbConnection) MarshalObject(object interface{}) (data []byte, err error) {
// Special case for the VERSION bucket. Here we're not using json
if v, ok := object.(string); ok {
return []byte(v), nil
data = []byte(v)
} else {
data, err = json.Marshal(object)
if err != nil {
return data, err
}
}
return json.Marshal(object)
if connection.getEncryptionKey() == nil {
return data, nil
}
return encrypt(data, connection.getEncryptionKey())
}
// UnmarshalObject decodes an object from binary data
func UnmarshalObject(data []byte, object interface{}) error {
// Special case for the VERSION bucket. Here we're not using json
// So we need to return it as a string
err := json.Unmarshal(data, object)
func (connection *DbConnection) UnmarshalObject(data []byte, object interface{}) error {
var err error
if connection.getEncryptionKey() != nil {
data, err = decrypt(data, connection.getEncryptionKey())
if err != nil {
return errors.Wrap(err, "Failed decrypting object")
}
}
e := json.Unmarshal(data, object)
if e != nil {
// Special case for the VERSION bucket. Here we're not using json
// So we need to return it as a string
s, ok := object.(*string)
if !ok {
return errors.Wrap(err, e.Error())
}
*s = string(data)
}
return err
}
// UnmarshalObjectWithJsoniter decodes an object from binary data
// using the jsoniter library. It is mainly used to accelerate environment(endpoint)
// decoding at the moment.
func (connection *DbConnection) UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
if connection.getEncryptionKey() != nil {
var err error
data, err = decrypt(data, connection.getEncryptionKey())
if err != nil {
return err
}
}
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
err := jsoni.Unmarshal(data, &object)
if err != nil {
if s, ok := object.(*string); ok {
*s = string(data)
@@ -33,10 +79,55 @@ func UnmarshalObject(data []byte, object interface{}) error {
return nil
}
// UnmarshalObjectWithJsoniter decodes an object from binary data
// using the jsoniter library. It is mainly used to accelerate environment(endpoint)
// decoding at the moment.
func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
return jsoni.Unmarshal(data, &object)
// mmm, don't have a KMS .... aes GCM seems the most likely from
// https://gist.github.com/atoponce/07d8d4c833873be2f68c34f9afc5a78a#symmetric-encryption
func encrypt(plaintext []byte, passphrase []byte) (encrypted []byte, err error) {
block, _ := aes.NewCipher(passphrase)
gcm, err := cipher.NewGCM(block)
if err != nil {
return encrypted, err
}
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return encrypted, err
}
ciphertextByte := gcm.Seal(
nonce,
nonce,
plaintext,
nil)
return ciphertextByte, nil
}
func decrypt(encrypted []byte, passphrase []byte) (plaintextByte []byte, err error) {
if string(encrypted) == "false" {
return []byte("false"), nil
}
block, err := aes.NewCipher(passphrase)
if err != nil {
return encrypted, errors.Wrap(err, "Error creating cypher block")
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return encrypted, errors.Wrap(err, "Error creating GCM")
}
nonceSize := gcm.NonceSize()
if len(encrypted) < nonceSize {
return encrypted, errEncryptedStringTooShort
}
nonce, ciphertextByteClean := encrypted[:nonceSize], encrypted[nonceSize:]
plaintextByte, err = gcm.Open(
nil,
nonce,
ciphertextByteClean,
nil)
if err != nil {
return encrypted, errors.Wrap(err, "Error decrypting text")
}
return plaintextByte, err
}

View File

@@ -1,6 +1,7 @@
package boltdb
import (
"crypto/sha256"
"fmt"
"testing"
@@ -8,9 +9,17 @@ import (
"github.com/stretchr/testify/assert"
)
const jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
const (
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
passphrase = "my secret key"
)
func Test_MarshalObject(t *testing.T) {
func secretToEncryptionKey(passphrase string) []byte {
hash := sha256.Sum256([]byte(passphrase))
return hash[:]
}
func Test_MarshalObjectUnencrypted(t *testing.T) {
is := assert.New(t)
uuid := uuid.Must(uuid.NewV4())
@@ -73,16 +82,18 @@ func Test_MarshalObject(t *testing.T) {
},
}
conn := DbConnection{}
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
data, err := MarshalObject(test.object)
data, err := conn.MarshalObject(test.object)
is.NoError(err)
is.Equal(test.expected, string(data))
})
}
}
func Test_UnMarshalObject(t *testing.T) {
func Test_UnMarshalObjectUnencrypted(t *testing.T) {
is := assert.New(t)
// Based on actual data entering and what we expect out of the function
@@ -105,18 +116,62 @@ func Test_UnMarshalObject(t *testing.T) {
expected: "9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6",
},
{
// An unmarshalled json object string should return the same as a string without error also
// An un-marshalled json object string should return the same as a string without error also
object: []byte(jsonobject),
expected: jsonobject,
},
}
conn := DbConnection{}
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
var object string
err := UnmarshalObject(test.object, &object)
err := conn.UnmarshalObject(test.object, &object)
is.NoError(err)
is.Equal(test.expected, string(object))
})
}
}
func Test_ObjectMarshallingEncrypted(t *testing.T) {
is := assert.New(t)
// Based on actual data entering and what we expect out of the function
tests := []struct {
object []byte
expected string
}{
{
object: []byte(""),
},
{
object: []byte("35"),
},
{
// An unmarshalled byte string should return the same without error
object: []byte("9ca4a1dd-a439-4593-b386-a7dfdc2e9fc6"),
},
{
// An un-marshalled json object string should return the same as a string without error also
object: []byte(jsonobject),
},
}
key := secretToEncryptionKey(passphrase)
conn := DbConnection{EncryptionKey: key}
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
data, err := conn.MarshalObject(test.object)
is.NoError(err)
var object []byte
err = conn.UnmarshalObject(data, &object)
is.NoError(err)
is.Equal(test.object, object)
})
}
}

View File

@@ -2,15 +2,19 @@ package database
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
)
// NewDatabase should use config options to return a connection to the requested database
func NewDatabase(storeType, storePath string) (connection portainer.Connection, err error) {
func NewDatabase(storeType, storePath string, encryptionKey []byte) (connection portainer.Connection, err error) {
switch storeType {
case "boltdb":
return &boltdb.DbConnection{Path: storePath}, nil
return &boltdb.DbConnection{
Path: storePath,
EncryptionKey: encryptionKey,
}, nil
}
return nil, fmt.Errorf("Unknown storage database: %s", storeType)
return nil, fmt.Errorf("unknown storage database: %s", storeType)
}

View File

@@ -67,14 +67,14 @@ func (service *Service) EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, err
return &edgeJob, nil
}
// CreateEdgeJob creates a new Edge job
func (service *Service) Create(edgeJob *portainer.EdgeJob) error {
return service.connection.CreateObject(
// Create creates a new EdgeJob
func (service *Service) Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error {
edgeJob.ID = ID
return service.connection.CreateObjectWithId(
BucketName,
func(id uint64) (int, interface{}) {
edgeJob.ID = portainer.EdgeJobID(id)
return int(edgeJob.ID), edgeJob
},
int(edgeJob.ID),
edgeJob,
)
}

View File

@@ -1,6 +1,7 @@
package endpoint
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
@@ -69,7 +70,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
endpoint, ok := obj.(*portainer.Endpoint)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Endpoint object")
return nil, fmt.Errorf("Failed to convert to Endpoint object: %s", obj)
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
}
endpoints = append(endpoints, *endpoint)
return &portainer.Endpoint{}, nil
@@ -83,6 +84,15 @@ func (service *Service) Create(endpoint *portainer.Endpoint) error {
return service.connection.CreateObjectWithSetSequence(BucketName, int(endpoint.ID), endpoint)
}
// CreateEndpoint assign an ID to a new environment(endpoint) and saves it.
func (service *Service) CreateWithCallback(endpoint *portainer.Endpoint, fn func(id uint64) (int, interface{})) error {
if endpoint.ID > 0 {
return errors.New("the endpoint must not have an ID")
}
return service.connection.CreateObject(BucketName, fn)
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
return service.connection.GetNextIdentifier(BucketName)

View File

@@ -4,6 +4,7 @@ import "errors"
var (
// TODO: i'm pretty sure this needs wrapping at several levels
ErrObjectNotFound = errors.New("Object not found inside the database")
ErrWrongDBEdition = errors.New("The Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrObjectNotFound = errors.New("object not found inside the database")
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrDBImportFailed = errors.New("importing backup failed")
)

View File

@@ -0,0 +1,93 @@
package fdoprofile
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "fdo_profiles"
)
// Service represents a service for managingFDO Profiles data.
type Service struct {
connection portainer.Connection
}
func (service *Service) BucketName() string {
return BucketName
}
// NewService creates a new instance of a service.
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
// FDOProfiles return an array containing all the FDO Profiles.
func (service *Service) FDOProfiles() ([]portainer.FDOProfile, error) {
var fdoProfiles = make([]portainer.FDOProfile, 0)
err := service.connection.GetAll(
BucketName,
&portainer.FDOProfile{},
func(obj interface{}) (interface{}, error) {
fdoProfile, ok := obj.(*portainer.FDOProfile)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to FDOProfile object")
return nil, fmt.Errorf("failed to convert to FDOProfile object: %s", obj)
}
fdoProfiles = append(fdoProfiles, *fdoProfile)
return &portainer.FDOProfile{}, nil
})
return fdoProfiles, err
}
// FDOProfile returns an FDO Profile by ID.
func (service *Service) FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error) {
var FDOProfile portainer.FDOProfile
identifier := service.connection.ConvertToKey(int(ID))
err := service.connection.GetObject(BucketName, identifier, &FDOProfile)
if err != nil {
return nil, err
}
return &FDOProfile, nil
}
// Create assign an ID to a new FDO Profile and saves it.
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
return service.connection.CreateObjectWithId(
BucketName,
int(FDOProfile.ID),
FDOProfile,
)
}
// Update updates an FDO Profile.
func (service *Service) Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.UpdateObject(BucketName, identifier, FDOProfile)
}
// Delete deletes an FDO Profile.
func (service *Service) Delete(ID portainer.FDOProfileID) error {
identifier := service.connection.ConvertToKey(int(ID))
return service.connection.DeleteObject(BucketName, identifier)
}
// GetNextIdentifier returns the next identifier for a FDO Profile.
func (service *Service) GetNextIdentifier() int {
return service.connection.GetNextIdentifier(BucketName)
}

View File

@@ -34,7 +34,7 @@ func NewService(connection portainer.Connection) (*Service, error) {
}
//HelmUserRepository returns an array of all HelmUserRepository
func (service *Service) HelmUserRepositorys() ([]portainer.HelmUserRepository, error) {
func (service *Service) HelmUserRepositories() ([]portainer.HelmUserRepository, error) {
var repos = make([]portainer.HelmUserRepository, 0)
err := service.connection.GetAll(

View File

@@ -31,6 +31,7 @@ type (
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
FDOProfile() FDOProfileService
HelmUserRepository() HelmUserRepositoryService
Registry() RegistryService
ResourceControl() ResourceControlService
@@ -73,7 +74,7 @@ type (
EdgeJobService interface {
EdgeJobs() ([]portainer.EdgeJob, error)
EdgeJob(ID portainer.EdgeJobID) (*portainer.EdgeJob, error)
Create(edgeJob *portainer.EdgeJob) error
Create(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
UpdateEdgeJob(ID portainer.EdgeJobID, edgeJob *portainer.EdgeJob) error
DeleteEdgeJob(ID portainer.EdgeJobID) error
GetNextIdentifier() int
@@ -96,6 +97,7 @@ type (
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
Endpoints() ([]portainer.Endpoint, error)
Create(endpoint *portainer.Endpoint) error
CreateWithCallback(endpoint *portainer.Endpoint, fn func(uint64) (int, interface{})) error
UpdateEndpoint(ID portainer.EndpointID, endpoint *portainer.Endpoint) error
DeleteEndpoint(ID portainer.EndpointID) error
GetNextIdentifier() int
@@ -122,9 +124,20 @@ type (
BucketName() string
}
// FDOProfileService represents a service to manage FDO Profiles
FDOProfileService interface {
FDOProfiles() ([]portainer.FDOProfile, error)
FDOProfile(ID portainer.FDOProfileID) (*portainer.FDOProfile, error)
Create(FDOProfile *portainer.FDOProfile) error
Update(ID portainer.FDOProfileID, FDOProfile *portainer.FDOProfile) error
Delete(ID portainer.FDOProfileID) error
GetNextIdentifier() int
BucketName() string
}
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
HelmUserRepositoryService interface {
HelmUserRepositorys() ([]portainer.HelmUserRepository, error)
HelmUserRepositories() ([]portainer.HelmUserRepository, error)
HelmUserRepositoryByUserID(userID portainer.UserID) ([]portainer.HelmUserRepository, error)
Create(record *portainer.HelmUserRepository) error
UpdateHelmUserRepository(ID portainer.HelmUserRepositoryID, repository *portainer.HelmUserRepository) error

View File

@@ -4,7 +4,6 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
)
@@ -79,9 +78,6 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
if err == stop {
return resourceControl, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}

View File

@@ -1,6 +1,8 @@
package settings
import (
"sync"
portainer "github.com/portainer/portainer/api"
)
@@ -13,6 +15,30 @@ const (
// Service represents a service for managing environment(endpoint) data.
type Service struct {
connection portainer.Connection
cache *portainer.Settings
mu sync.RWMutex
}
func cloneSettings(src *portainer.Settings) *portainer.Settings {
if src == nil {
return nil
}
c := *src
if c.BlackListedLabels != nil {
c.BlackListedLabels = make([]portainer.Pair, len(src.BlackListedLabels))
copy(c.BlackListedLabels, src.BlackListedLabels)
}
if src.FeatureFlagSettings != nil {
c.FeatureFlagSettings = make(map[portainer.Feature]bool)
for k, v := range src.FeatureFlagSettings {
c.FeatureFlagSettings[k] = v
}
}
return &c
}
func (service *Service) BucketName() string {
@@ -33,6 +59,18 @@ func NewService(connection portainer.Connection) (*Service, error) {
// Settings retrieve the settings object.
func (service *Service) Settings() (*portainer.Settings, error) {
service.mu.RLock()
if service.cache != nil {
s := cloneSettings(service.cache)
service.mu.RUnlock()
return s, nil
}
service.mu.RUnlock()
service.mu.Lock()
defer service.mu.Unlock()
var settings portainer.Settings
err := service.connection.GetObject(BucketName, []byte(settingsKey), &settings)
@@ -40,12 +78,24 @@ func (service *Service) Settings() (*portainer.Settings, error) {
return nil, err
}
service.cache = cloneSettings(&settings)
return &settings, nil
}
// UpdateSettings persists a Settings object.
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
service.mu.Lock()
defer service.mu.Unlock()
err := service.connection.UpdateObject(BucketName, []byte(settingsKey), settings)
if err != nil {
return err
}
service.cache = cloneSettings(settings)
return nil
}
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
@@ -61,3 +111,9 @@ func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
return false
}
func (service *Service) InvalidateCache() {
service.mu.Lock()
service.cache = nil
service.mu.Unlock()
}

View File

@@ -29,7 +29,7 @@ func TestService_StackByWebhookID(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
b := stackBuilder{t: t, store: store}
@@ -87,7 +87,7 @@ func Test_RefreshableStacks(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
staticStack := portainer.Stack{ID: 1}

View File

@@ -10,7 +10,7 @@ import (
func Test_teamByName(t *testing.T) {
t.Run("When store is empty should return ErrObjectNotFound", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
_, err := store.Team().TeamByName("name")
@@ -19,7 +19,7 @@ func Test_teamByName(t *testing.T) {
})
t.Run("When there is no object with the same name should return ErrObjectNotFound", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
teamBuilder := teamBuilder{
@@ -35,7 +35,7 @@ func Test_teamByName(t *testing.T) {
})
t.Run("When there is an object with the same name should return the object", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(true)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
teamBuilder := teamBuilder{

View File

@@ -35,7 +35,7 @@ func (store *Store) createBackupFolders() {
}
func (store *Store) databasePath() string {
return path.Join(store.connection.GetStorePath(), store.connection.GetDatabaseFilename())
return store.connection.GetDatabaseFilePath()
}
func (store *Store) commonBackupDir() string {
@@ -69,6 +69,11 @@ func getBackupRestoreOptions(backupDir string) *BackupOptions {
}
}
// Backup current database with default options
func (store *Store) Backup() (string, error) {
return store.backupWithOptions(nil)
}
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
if options == nil {
options = &BackupOptions{}
@@ -84,7 +89,7 @@ func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
options.BackupDir = store.commonBackupDir()
}
if options.BackupFileName == "" {
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFilename(), fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
options.BackupFileName = fmt.Sprintf("%s.%s.%s", store.connection.GetDatabaseFileName(), fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
}
if options.BackupPath == "" {
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)

View File

@@ -10,7 +10,7 @@ import (
)
func TestCreateBackupFolders(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(false, true)
defer teardown()
connection := store.GetConnection()
@@ -27,7 +27,7 @@ func TestCreateBackupFolders(t *testing.T) {
}
func TestStoreCreation(t *testing.T) {
_, store, teardown := MustNewTestStore(true)
_, store, teardown := MustNewTestStore(true, true)
defer teardown()
if store == nil {
@@ -40,7 +40,7 @@ func TestStoreCreation(t *testing.T) {
}
func TestBackup(t *testing.T) {
_, store, teardown := MustNewTestStore(true)
_, store, teardown := MustNewTestStore(true, true)
connection := store.GetConnection()
defer teardown()
@@ -48,7 +48,7 @@ func TestBackup(t *testing.T) {
store.VersionService.StoreDBVersion(portainer.DBVersion)
store.backupWithOptions(nil)
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.db.%03d.*", portainer.DBVersion))
backupFileName := path.Join(connection.GetStorePath(), "backups", "common", fmt.Sprintf("portainer.edb.%03d.*", portainer.DBVersion))
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}
@@ -67,7 +67,7 @@ func TestBackup(t *testing.T) {
}
func TestRemoveWithOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(true)
_, store, teardown := MustNewTestStore(true, true)
defer teardown()
t.Run("successfully removes file if existent", func(t *testing.T) {
@@ -86,7 +86,7 @@ func TestRemoveWithOptions(t *testing.T) {
err = store.removeWithOptions(options)
if err != nil {
t.Errorf("RemoveWithOptions should successfully remove file; err=%w", err)
t.Errorf("RemoveWithOptions should successfully remove file; err=%v", err)
}
if isFileExist(f.Name()) {

View File

@@ -1,10 +1,15 @@
package datastore
import (
"fmt"
"io"
"os"
"path"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
portainerErrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
)
func (store *Store) version() (int, error) {
@@ -34,6 +39,19 @@ func NewStore(storePath string, fileService portainer.FileService, connection po
// Open opens and initializes the BoltDB database.
func (store *Store) Open() (newStore bool, err error) {
newStore = true
encryptionReq, err := store.connection.NeedsEncryptionMigration()
if err != nil {
return false, err
}
if encryptionReq {
err = store.encryptDB()
if err != nil {
return false, err
}
}
err = store.connection.Open()
if err != nil {
return newStore, err
@@ -45,8 +63,18 @@ func (store *Store) Open() (newStore bool, err error) {
}
// if we have DBVersion in the database then ensure we flag this as NOT a new store
if _, err := store.VersionService.DBVersion(); err == nil {
newStore = false
version, err := store.VersionService.DBVersion()
if err != nil {
if store.IsErrObjectNotFound(err) {
return newStore, nil
}
return newStore, err
}
if version > 0 {
logrus.WithField("version", version).Infof("Opened existing store")
return false, nil
}
return newStore, nil
@@ -65,16 +93,81 @@ func (store *Store) BackupTo(w io.Writer) error {
// CheckCurrentEdition checks if current edition is community edition
func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.PortainerCE {
return errors.ErrWrongDBEdition
return portainerErrors.ErrWrongDBEdition
}
return nil
}
// TODO: move the use of this to dataservices.IsErrObjectNotFound()?
func (store *Store) IsErrObjectNotFound(e error) bool {
return e == errors.ErrObjectNotFound
return e == portainerErrors.ErrObjectNotFound
}
func (store *Store) Rollback(force bool) error {
return store.connectionRollback(force)
}
func (store *Store) encryptDB() error {
store.connection.SetEncrypted(false)
err := store.connection.Open()
if err != nil {
return err
}
err = store.initServices()
if err != nil {
return err
}
// The DB is not currently encrypted. First save the encrypted db filename
oldFilename := store.connection.GetDatabaseFilePath()
logrus.Infof("Encrypting database")
// export file path for backup
exportFilename := path.Join(store.databasePath() + "." + fmt.Sprintf("backup-%d.json", time.Now().Unix()))
logrus.Infof("Exporting database backup to %s", exportFilename)
err = store.Export(exportFilename)
if err != nil {
logrus.WithError(err).Debugf("Failed to export to %s", exportFilename)
return err
}
logrus.Infof("Database backup exported")
// Close existing un-encrypted db so that we can delete the file later
store.connection.Close()
// Tell the db layer to create an encrypted db when opened
store.connection.SetEncrypted(true)
store.connection.Open()
// We have to init services before import
err = store.initServices()
if err != nil {
return err
}
err = store.Import(exportFilename)
if err != nil {
// Remove the new encrypted file that we failed to import
os.Remove(store.connection.GetDatabaseFilePath())
logrus.Fatal(portainerErrors.ErrDBImportFailed.Error())
}
err = os.Remove(oldFilename)
if err != nil {
logrus.Errorf("Failed to remove the un-encrypted db file")
}
err = os.Remove(exportFilename)
if err != nil {
logrus.Errorf("Failed to remove the json backup file")
}
// Close db connection
store.connection.Close()
logrus.Info("Database successfully encrypted")
return nil
}

View File

@@ -27,7 +27,7 @@ const (
// TestStoreFull an eventually comprehensive set of tests for the Store.
// The idea is what we write to the store, we should read back.
func TestStoreFull(t *testing.T) {
_, store, teardown := MustNewTestStore(true)
_, store, teardown := MustNewTestStore(true, true)
defer teardown()
testCases := map[string]func(t *testing.T){
@@ -96,7 +96,6 @@ func newEndpoint(endpointType portainer.EndpointType, id portainer.EndpointID, n
},
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
Extensions: []portainer.EndpointExtension{},
TagIDs: []portainer.TagID{},
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.DockerSnapshot{},
@@ -178,7 +177,7 @@ func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType porta
func (store *Store) CreateEndpointRelation(id portainer.EndpointID) {
relation := &portainer.EndpointRelation{
EndpointID: id,
EdgeStacks: map[portainer.EdgeStackID]bool{},
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
}
store.EndpointRelation().Create(relation)

View File

@@ -7,26 +7,44 @@ import (
// Init creates the default data set.
func (store *Store) Init() error {
instanceID, err := store.VersionService.InstanceID()
err := store.checkOrCreateInstanceID()
if err != nil {
return err
}
err = store.checkOrCreateDefaultSettings()
if err != nil {
return err
}
err = store.checkOrCreateDefaultSSLSettings()
if err != nil {
return err
}
return store.checkOrCreateDefaultData()
}
func (store *Store) checkOrCreateInstanceID() error {
_, err := store.VersionService.InstanceID()
if store.IsErrObjectNotFound(err) {
uid, err := uuid.NewV4()
if err != nil {
return err
}
instanceID = uid.String()
err = store.VersionService.StoreInstanceID(instanceID)
if err != nil {
return err
}
} else if err != nil {
return err
instanceID := uid.String()
return store.VersionService.StoreInstanceID(instanceID)
}
return err
}
func (store *Store) checkOrCreateDefaultSettings() error {
// TODO: these need to also be applied when importing
settings, err := store.SettingsService.Settings()
if store.IsErrObjectNotFound(err) {
defaultSettings := &portainer.Settings{
EnableTelemetry: true,
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
LDAPSettings: portainer.LDAPSettings{
@@ -34,14 +52,16 @@ func (store *Store) Init() error {
AutoCreateUsers: true,
TLSConfig: portainer.TLSConfiguration{},
SearchSettings: []portainer.LDAPSearchSettings{
portainer.LDAPSearchSettings{},
{},
},
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
portainer.LDAPGroupSearchSettings{},
{},
},
},
OAuthSettings: portainer.OAuthSettings{},
OAuthSettings: portainer.OAuthSettings{
SSO: true,
},
SnapshotInterval: portainer.DefaultSnapshotInterval,
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
TemplatesURL: portainer.DefaultTemplatesURL,
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
@@ -50,35 +70,33 @@ func (store *Store) Init() error {
KubectlShellImage: portainer.DefaultKubectlShellImage,
}
err = store.SettingsService.UpdateSettings(defaultSettings)
if err != nil {
return err
}
} else if err != nil {
return store.SettingsService.UpdateSettings(defaultSettings)
}
if err != nil {
return err
} else if err == nil {
if settings.UserSessionTimeout == "" {
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
store.Settings().UpdateSettings(settings)
}
}
_, err = store.SSLSettings().Settings()
if err != nil {
if !store.IsErrObjectNotFound(err) {
return err
}
if settings.UserSessionTimeout == "" {
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
return store.Settings().UpdateSettings(settings)
}
return nil
}
func (store *Store) checkOrCreateDefaultSSLSettings() error {
_, err := store.SSLSettings().Settings()
if store.IsErrObjectNotFound(err) {
defaultSSLSettings := &portainer.SSLSettings{
HTTPEnabled: true,
}
err = store.SSLSettings().UpdateSettings(defaultSSLSettings)
if err != nil {
return err
}
return store.SSLSettings().UpdateSettings(defaultSSLSettings)
}
return err
}
func (store *Store) checkOrCreateDefaultData() error {
groups, err := store.EndpointGroupService.EndpointGroups()
if err != nil {
return err
@@ -99,6 +117,5 @@ func (store *Store) Init() error {
return err
}
}
return nil
}

View File

@@ -28,10 +28,20 @@ func (slog *ScopedLog) Debug(message string) {
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Debugf(message string, vars ...interface{}) {
message = fmt.Sprintf(message, vars...)
slog.print(DEBUG, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Info(message string) {
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Infof(message string, vars ...interface{}) {
message = fmt.Sprintf(message, vars...)
slog.print(INFO, fmt.Sprintf("[message: %s]", message))
}
func (slog *ScopedLog) Error(message string, err error) {
slog.print(ERROR, fmt.Sprintf("[message: %s] [error: %s]", message, err))
}

View File

@@ -9,6 +9,7 @@ import (
plog "github.com/portainer/portainer/api/datastore/log"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/sirupsen/logrus"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
@@ -24,12 +25,21 @@ func (store *Store) MigrateData() error {
return err
}
// Backup Database
backupPath, err := store.Backup()
if err != nil {
return werrors.Wrap(err, "while backing up db before migration")
}
store.SettingsService.InvalidateCache()
migratorParams := &migrator.MigratorParameters{
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
FDOProfilesService: store.FDOProfilesService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
@@ -45,7 +55,27 @@ func (store *Store) MigrateData() error {
AuthorizationService: authorization.NewService(store),
}
return store.connectionMigrateData(migratorParams)
// restore on error
err = store.connectionMigrateData(migratorParams)
if err != nil {
logrus.Errorf("While DB migration %v. Restoring DB", err)
// Restore options
options := BackupOptions{
BackupPath: backupPath,
}
err := store.restoreWithOptions(&options)
if err != nil {
logrus.Fatalf(
"Failed restoring the backup. portainer database file needs to restored manually by "+
"replacing %s database file with recent backup %s. Error %v",
store.databasePath(),
options.BackupPath,
err,
)
}
}
return err
}
// FailSafeMigrate backup and restore DB if migration fail

View File

@@ -1,13 +1,19 @@
package datastore
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
)
// testVersion is a helper which tests current store version against wanted version
@@ -22,8 +28,32 @@ func testVersion(store *Store, versionWant int, t *testing.T) {
}
func TestMigrateData(t *testing.T) {
snapshotTests := []struct {
testName string
srcPath string
wantPath string
}{
{
testName: "migrate version 24 to 35",
srcPath: "test_data/input_24.json",
wantPath: "test_data/output_35.json",
},
}
for _, test := range snapshotTests {
t.Run(test.testName, func(t *testing.T) {
err := migrateDBTestHelper(t, test.srcPath, test.wantPath)
if err != nil {
t.Errorf(
"Failed migrating mock database %v: %v",
test.srcPath,
err,
)
}
})
}
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
newStore, store, teardown := MustNewTestStore(false)
newStore, store, teardown := MustNewTestStore(false, true)
defer teardown()
if !newStore {
@@ -50,7 +80,7 @@ func TestMigrateData(t *testing.T) {
{version: 21, expectedVersion: portainer.DBVersion},
}
for _, tc := range tests {
_, store, teardown := MustNewTestStore(true)
_, store, teardown := MustNewTestStore(true, true)
defer teardown()
// Setup data
@@ -75,7 +105,7 @@ func TestMigrateData(t *testing.T) {
}
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(false, true)
defer teardown()
version := 17
@@ -87,7 +117,7 @@ func TestMigrateData(t *testing.T) {
})
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(false, true)
defer teardown()
store.VersionService.StoreDBVersion(0)
@@ -101,7 +131,7 @@ func TestMigrateData(t *testing.T) {
})
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(false, true)
defer teardown()
store.VersionService.StoreIsUpdating(true)
@@ -116,7 +146,7 @@ func TestMigrateData(t *testing.T) {
})
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(false, true)
defer teardown()
store.MigrateData()
@@ -131,7 +161,7 @@ func TestMigrateData(t *testing.T) {
}
func Test_getBackupRestoreOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(false, true)
defer teardown()
options := getBackupRestoreOptions(store.commonBackupDir())
@@ -150,7 +180,7 @@ func Test_getBackupRestoreOptions(t *testing.T) {
func TestRollback(t *testing.T) {
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
version := 21
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(false, true)
defer teardown()
store.VersionService.StoreDBVersion(version)
@@ -185,3 +215,250 @@ func isFileExist(path string) bool {
}
return len(matches) > 0
}
// migrateDBTestHelper loads a json representation of a bolt database from srcPath,
// parses it into a database, runs a migration on that database, and then
// compares it with an expected output database.
func migrateDBTestHelper(t *testing.T, srcPath, wantPath string) error {
srcJSON, err := os.ReadFile(srcPath)
if err != nil {
t.Fatalf("failed loading source JSON file %v: %v", srcPath, err)
}
// Parse source json to db.
_, store, teardown := MustNewTestStore(true, false)
defer teardown()
err = importJSON(t, bytes.NewReader(srcJSON), store)
if err != nil {
return err
}
// Run the actual migrations on our input database.
err = store.MigrateData()
if err != nil {
return err
}
// Assert that our database connection is using bolt so we can call
// exportJson rather than ExportRaw. The exportJson function allows us to
// strip out the metadata which we don't want for our tests.
// TODO: update connection interface in CE to allow us to use ExportRaw and pass meta false
err = store.connection.Close()
if err != nil {
t.Fatalf("err closing bolt connection: %v", err)
}
con, ok := store.connection.(*boltdb.DbConnection)
if !ok {
t.Fatalf("backing database is not using boltdb, but the migrations test requires it")
}
// Convert database back to json.
databasePath := con.GetDatabaseFilePath()
if _, err := os.Stat(databasePath); err != nil {
return fmt.Errorf("stat on %s failed: %s", databasePath, err)
}
gotJSON, err := con.ExportJson(databasePath, false)
if err != nil {
t.Logf(
"failed re-exporting database %s to JSON: %v",
databasePath,
err,
)
}
wantJSON, err := os.ReadFile(wantPath)
if err != nil {
t.Fatalf("failed loading want JSON file %v: %v", wantPath, err)
}
// Compare the result we got with the one we wanted.
if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
os.WriteFile(
gotPath,
gotJSON,
0600,
)
t.Errorf(
"migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
srcPath,
wantPath,
gotPath,
diff,
)
}
return nil
}
// importJSON reads input JSON and commits it to a portainer datastore.Store.
// Errors are logged with the testing package.
func importJSON(t *testing.T, r io.Reader, store *Store) error {
objects := make(map[string]interface{})
// Parse json into map of objects.
d := json.NewDecoder(r)
d.UseNumber()
err := d.Decode(&objects)
if err != nil {
return err
}
// Get database connection from store.
con := store.connection
for k, v := range objects {
switch k {
case "version":
versions, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed casting %s to map[string]interface{}", k)
}
dbVersion, ok := versions["DB_VERSION"]
if !ok {
t.Logf("failed getting DB_VERSION from %s", k)
}
numDBVersion, ok := dbVersion.(json.Number)
if !ok {
t.Logf("failed parsing DB_VERSION as json number from %s", k)
}
intDBVersion, err := numDBVersion.Int64()
if err != nil {
t.Logf("failed casting %v to int: %v", numDBVersion, intDBVersion)
}
err = con.CreateObjectWithStringId(
k,
[]byte("DB_VERSION"),
int(intDBVersion),
)
if err != nil {
t.Logf("failed writing DB_VERSION in %s: %v", k, err)
}
instanceID, ok := versions["INSTANCE_ID"]
if !ok {
t.Logf("failed getting INSTANCE_ID from %s", k)
}
err = con.CreateObjectWithStringId(
k,
[]byte("INSTANCE_ID"),
instanceID,
)
if err != nil {
t.Logf("failed writing INSTANCE_ID in %s: %v", k, err)
}
case "dockerhub":
obj, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("DOCKERHUB"),
obj[0],
)
if err != nil {
t.Logf("failed writing DOCKERHUB in %s: %v", k, err)
}
case "ssl":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("SSL"),
obj,
)
if err != nil {
t.Logf("failed writing SSL in %s: %v", k, err)
}
case "settings":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("SETTINGS"),
obj,
)
if err != nil {
t.Logf("failed writing SETTINGS in %s: %v", k, err)
}
case "tunnel_server":
obj, ok := v.(map[string]interface{})
if !ok {
t.Logf("failed to case %s to map[string]interface{}", k)
}
err := con.CreateObjectWithStringId(
k,
[]byte("INFO"),
obj,
)
if err != nil {
t.Logf("failed writing INFO in %s: %v", k, err)
}
case "templates":
continue
default:
objlist, ok := v.([]interface{})
if !ok {
t.Logf("failed to cast %s to []interface{}", k)
}
for _, obj := range objlist {
value, ok := obj.(map[string]interface{})
if !ok {
t.Logf("failed to cast %v to map[string]interface{}", obj)
} else {
var ok bool
var id interface{}
switch k {
case "endpoint_relations":
// TODO: need to make into an int, then do that weird
// stringification
id, ok = value["EndpointID"]
default:
id, ok = value["Id"]
}
if !ok {
// endpoint_relations: EndpointID
t.Logf("missing Id field: %s", k)
id = "error"
}
n, ok := id.(json.Number)
if !ok {
t.Logf("failed to cast %v to json.Number in %s", id, k)
} else {
key, err := n.Int64()
if err != nil {
t.Logf("failed to cast %v to int in %s", n, k)
} else {
err := con.CreateObjectWithId(
k,
int(key),
value,
)
if err != nil {
t.Logf("failed writing %v in %s: %v", key, k, err)
}
}
}
}
}
}
}
return nil
}

View File

@@ -33,7 +33,7 @@ func setup(store *Store) error {
}
func TestMigrateSettings(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(false, true)
defer teardown()
err := setup(store)

View File

@@ -10,7 +10,7 @@ import (
)
func TestMigrateStackEntryPoint(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(false, true)
defer teardown()
stackService := store.Stack()

View File

@@ -1,16 +1,38 @@
package migrator
import (
"fmt"
"errors"
"reflect"
"runtime"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
type migration struct {
dbversion int
migrate func() error
}
func migrationError(err error, context string) error {
return werrors.Wrap(err, "failed in "+context)
}
func newMigration(dbversion int, migrate func() error) migration {
return migration{
dbversion: dbversion,
migrate: migrate,
}
}
func dbTooOldError() error {
return errors.New("migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
}
func GetFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// set DB to updating status
@@ -19,175 +41,90 @@ func (m *Migrator) Migrate() error {
return migrationError(err, "StoreIsUpdating")
}
if m.currentDBVersion < 17 {
return migrationError(err, "migrating from less than Portainer 1.21.0 is not supported, please contact Portainer support.")
migrations := []migration{
// Portainer < 1.21.0
newMigration(17, dbTooOldError),
// Portainer 1.21.0
newMigration(18, m.updateUsersToDBVersion18),
newMigration(18, m.updateEndpointsToDBVersion18),
newMigration(18, m.updateEndpointGroupsToDBVersion18),
newMigration(18, m.updateRegistriesToDBVersion18),
// 1.22.0
newMigration(19, m.updateSettingsToDBVersion19),
// 1.22.1
newMigration(20, m.updateUsersToDBVersion20),
newMigration(20, m.updateSettingsToDBVersion20),
newMigration(20, m.updateSchedulesToDBVersion20),
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
newMigration(22, m.updateResourceControlsToDBVersion22),
newMigration(22, m.updateUsersAndRolesToDBVersion22),
// Portainer 1.24.0
newMigration(23, m.updateTagsToDBVersion23),
newMigration(23, m.updateEndpointsAndEndpointGroupsToDBVersion23),
// Portainer 1.24.1
newMigration(24, m.updateSettingsToDB24),
// Portainer 2.0.0
newMigration(25, m.updateSettingsToDB25),
newMigration(25, m.updateStacksToDB24), // yes this looks odd. Don't be tempted to move it
// Portainer 2.1.0
newMigration(26, m.updateEndpointSettingsToDB25),
// Portainer 2.2.0
newMigration(27, m.updateStackResourceControlToDB27),
// Portainer 2.6.0
newMigration(30, m.migrateDBVersionToDB30),
// Portainer 2.9.0
newMigration(32, m.migrateDBVersionToDB32),
// Portainer 2.9.1, 2.9.2
newMigration(33, m.migrateDBVersionToDB33),
// Portainer 2.10
newMigration(34, m.migrateDBVersionToDB34),
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
newMigration(35, m.migrateDBVersionToDB35),
newMigration(36, m.migrateDBVersionToDB36),
// Portainer 2.13
newMigration(40, m.migrateDBVersionToDB40),
}
// Portainer 1.21.0
if m.currentDBVersion < 18 {
err := m.updateUsersToDBVersion18()
if err != nil {
return migrationError(err, "updateUsersToDBVersion18")
}
var lastDbVersion int
for _, migration := range migrations {
if m.currentDBVersion < migration.dbversion {
err = m.updateEndpointsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointsToDBVersion18")
}
// Print the next line only when the version changes
if migration.dbversion > lastDbVersion {
migrateLog.Infof("Migrating DB to version %d", migration.dbversion)
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointGroupsToDBVersion18")
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return migrationError(err, "updateRegistriesToDBVersion18")
}
}
// Portainer 1.22.0
if m.currentDBVersion < 19 {
err := m.updateSettingsToDBVersion19()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion19")
}
}
// Portainer 1.22.1
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return migrationError(err, "updateUsersToDBVersion20")
}
err = m.updateSettingsToDBVersion20()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion20")
}
err = m.updateSchedulesToDBVersion20()
if err != nil {
return migrationError(err, "updateSchedulesToDBVersion20")
}
}
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
if m.currentDBVersion < 22 {
err := m.updateResourceControlsToDBVersion22()
if err != nil {
return migrationError(err, "updateResourceControlsToDBVersion22")
}
err = m.updateUsersAndRolesToDBVersion22()
if err != nil {
return migrationError(err, "updateUsersAndRolesToDBVersion22")
}
}
// Portainer 1.24.0
if m.currentDBVersion < 23 {
migrateLog.Info("Migrating to DB 23")
err := m.updateTagsToDBVersion23()
if err != nil {
return migrationError(err, "updateTagsToDBVersion23")
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return migrationError(err, "updateEndpointsAndEndpointGroupsToDBVersion23")
}
}
// Portainer 1.24.1
if m.currentDBVersion < 24 {
migrateLog.Info("Migrating to DB 24")
err := m.updateSettingsToDB24()
if err != nil {
return migrationError(err, "updateSettingsToDB24")
}
}
// Portainer 2.0.0
if m.currentDBVersion < 25 {
migrateLog.Info("Migrating to DB 25")
err := m.updateSettingsToDB25()
if err != nil {
return migrationError(err, "updateSettingsToDB25")
}
err = m.updateStacksToDB24()
if err != nil {
return migrationError(err, "updateStacksToDB24")
}
}
// Portainer 2.1.0
if m.currentDBVersion < 26 {
migrateLog.Info("Migrating to DB 26")
err := m.updateEndpointSettingsToDB25()
if err != nil {
return migrationError(err, "updateEndpointSettingsToDB25")
}
}
// Portainer 2.2.0
if m.currentDBVersion < 27 {
migrateLog.Info("Migrating to DB 27")
err := m.updateStackResourceControlToDB27()
if err != nil {
return migrationError(err, "updateStackResourceControlToDB27")
}
}
// Portainer 2.6.0
if m.currentDBVersion < 30 {
migrateLog.Info("Migrating to DB 30")
err := m.migrateDBVersionToDB30()
if err != nil {
return migrationError(err, "migrateDBVersionToDB30")
}
}
// Portainer 2.9.0
if m.currentDBVersion < 32 {
err := m.migrateDBVersionToDB32()
if err != nil {
return migrationError(err, "migrateDBVersionToDB32")
}
}
// Portainer 2.9.1, 2.9.2
if m.currentDBVersion < 33 {
migrateLog.Info("Migrating to DB 33")
err := m.migrateDBVersionToDB33()
if err != nil {
return migrationError(err, "migrateDBVersionToDB33")
}
}
// Portainer 2.10
if m.currentDBVersion < 34 {
migrateLog.Info("Migrating to DB 34")
if err := m.migrateDBVersionToDB34(); err != nil {
return migrationError(err, "migrateDBVersionToDB34")
}
}
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
if m.currentDBVersion < 35 {
migrateLog.Info("Migrating to DB 35")
if err := m.migrateDBVersionToDB35(); err != nil {
return migrationError(err, "migrateDBVersionToDB35")
err := migration.migrate()
if err != nil {
return migrationError(err, GetFunctionName(migration.migrate))
}
}
lastDbVersion = migration.dbversion
}
migrateLog.Infof("Setting DB version to %d", portainer.DBVersion)
err = m.versionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return migrationError(err, "StoreDBVersion")
}
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
migrateLog.Infof("Updated DB version to %d", portainer.DBVersion)
// reset DB updating status
return m.versionService.StoreIsUpdating(false)

View File

@@ -5,6 +5,7 @@ import (
)
func (m *Migrator) updateUsersToDBVersion18() error {
migrateLog.Info("- updating users")
legacyUsers, err := m.userService.Users()
if err != nil {
return err
@@ -39,6 +40,7 @@ func (m *Migrator) updateUsersToDBVersion18() error {
}
func (m *Migrator) updateEndpointsToDBVersion18() error {
migrateLog.Info("- updating endpoints")
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
@@ -69,6 +71,7 @@ func (m *Migrator) updateEndpointsToDBVersion18() error {
}
func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
migrateLog.Info("- updating endpoint groups")
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
@@ -99,6 +102,7 @@ func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
}
func (m *Migrator) updateRegistriesToDBVersion18() error {
migrateLog.Info("- updating registries")
legacyRegistries, err := m.registryService.Registries()
if err != nil {
return err

View File

@@ -3,6 +3,7 @@ package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDBVersion19() error {
migrateLog.Info("- updating settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err

View File

@@ -7,6 +7,7 @@ import (
const scheduleScriptExecutionJobType = 1
func (m *Migrator) updateUsersToDBVersion20() error {
migrateLog.Info("- updating user authentication")
return m.authorizationService.UpdateUsersAuthorizations()
}
@@ -22,6 +23,7 @@ func (m *Migrator) updateSettingsToDBVersion20() error {
}
func (m *Migrator) updateSchedulesToDBVersion20() error {
migrateLog.Info("- updating schedules")
legacySchedules, err := m.scheduleService.Schedules()
if err != nil {
return err

View File

@@ -6,6 +6,7 @@ import (
)
func (m *Migrator) updateResourceControlsToDBVersion22() error {
migrateLog.Info("- updating resource controls")
legacyResourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
@@ -24,6 +25,7 @@ func (m *Migrator) updateResourceControlsToDBVersion22() error {
}
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
migrateLog.Info("- updating users and roles")
legacyUsers, err := m.userService.Users()
if err != nil {
return err

View File

@@ -3,7 +3,7 @@ package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateTagsToDBVersion23() error {
migrateLog.Info("Updating tags")
migrateLog.Info("- Updating tags")
tags, err := m.tagService.Tags()
if err != nil {
return err
@@ -21,7 +21,7 @@ func (m *Migrator) updateTagsToDBVersion23() error {
}
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
migrateLog.Info("Updating endpoints and endpoint groups")
migrateLog.Info("- updating endpoints and endpoint groups")
tags, err := m.tagService.Tags()
if err != nil {
return err
@@ -54,7 +54,7 @@ func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
relation := &portainer.EndpointRelation{
EndpointID: endpoint.ID,
EdgeStacks: map[portainer.EdgeStackID]bool{},
EdgeStacks: map[portainer.EdgeStackID]portainer.EdgeStackStatus{},
}
err = m.endpointRelationService.Create(relation)

View File

@@ -3,7 +3,7 @@ package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateSettingsToDB24() error {
migrateLog.Info("Updating Settings")
migrateLog.Info("- updating Settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
@@ -18,7 +18,7 @@ func (m *Migrator) updateSettingsToDB24() error {
}
func (m *Migrator) updateStacksToDB24() error {
migrateLog.Info("Updating stacks")
migrateLog.Info("- updating stacks")
stacks, err := m.stackService.Stacks()
if err != nil {
return err

View File

@@ -5,7 +5,7 @@ import (
)
func (m *Migrator) updateSettingsToDB25() error {
migrateLog.Info("Updating settings")
migrateLog.Info("- updating settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {

View File

@@ -5,7 +5,7 @@ import (
)
func (m *Migrator) updateEndpointSettingsToDB25() error {
migrateLog.Info("Updating endpoint settings")
migrateLog.Info("- updating endpoint settings")
settings, err := m.settingsService.Settings()
if err != nil {
return err

View File

@@ -7,7 +7,7 @@ import (
)
func (m *Migrator) updateStackResourceControlToDB27() error {
migrateLog.Info("Updating stack resource controls")
migrateLog.Info("- updating stack resource controls")
resourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err

View File

@@ -1,7 +1,7 @@
package migrator
func (m *Migrator) migrateDBVersionToDB30() error {
migrateLog.Info("Updating legacy settings")
migrateLog.Info("- updating legacy settings")
if err := m.MigrateSettingsToDB30(); err != nil {
return err
}

View File

@@ -2,38 +2,34 @@ package migrator
import (
"fmt"
"github.com/portainer/portainer/api/dataservices/errors"
"log"
"github.com/portainer/portainer/api/dataservices/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
snapshotutils "github.com/portainer/portainer/api/internal/snapshot"
)
func (m *Migrator) migrateDBVersionToDB32() error {
migrateLog.Info("Updating registries")
err := m.updateRegistriesToDB32()
if err != nil {
return err
}
migrateLog.Info("Updating dockerhub")
err = m.updateDockerhubToDB32()
if err != nil {
return err
}
migrateLog.Info("Updating resource controls")
if err := m.updateVolumeResourceControlToDB32(); err != nil {
return err
}
migrateLog.Info("Updating kubeconfig expiry")
if err := m.kubeconfigExpiryToDB32(); err != nil {
return err
}
migrateLog.Info("Setting default helm repository url")
if err := m.helmRepositoryURLToDB32(); err != nil {
return err
}
@@ -42,6 +38,7 @@ func (m *Migrator) migrateDBVersionToDB32() error {
}
func (m *Migrator) updateRegistriesToDB32() error {
migrateLog.Info("- updating registries")
registries, err := m.registryService.Registries()
if err != nil {
return err
@@ -84,6 +81,7 @@ func (m *Migrator) updateRegistriesToDB32() error {
}
func (m *Migrator) updateDockerhubToDB32() error {
migrateLog.Info("- updating dockerhub")
dockerhub, err := m.dockerhubService.DockerHub()
if err == errors.ErrObjectNotFound {
return nil
@@ -172,6 +170,7 @@ func (m *Migrator) updateDockerhubToDB32() error {
}
func (m *Migrator) updateVolumeResourceControlToDB32() error {
migrateLog.Info("- updating resource controls")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return fmt.Errorf("failed fetching environments: %w", err)
@@ -264,6 +263,7 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
}
func (m *Migrator) kubeconfigExpiryToDB32() error {
migrateLog.Info("- updating kubeconfig expiry")
settings, err := m.settingsService.Settings()
if err != nil {
return err
@@ -273,6 +273,7 @@ func (m *Migrator) kubeconfigExpiryToDB32() error {
}
func (m *Migrator) helmRepositoryURLToDB32() error {
migrateLog.Info("- setting default helm repository URL")
settings, err := m.settingsService.Settings()
if err != nil {
return err

View File

@@ -1,8 +1,11 @@
package migrator
import portainer "github.com/portainer/portainer/api"
import (
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) migrateDBVersionToDB33() error {
migrateLog.Info("- updating settings")
if err := m.migrateSettingsToDB33(); err != nil {
return err
}
@@ -16,7 +19,7 @@ func (m *Migrator) migrateSettingsToDB33() error {
return err
}
migrateLog.Info("Setting default kubectl shell image")
migrateLog.Info("- setting default kubectl shell image")
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
return m.settingsService.UpdateSettings(settings)
}

View File

@@ -1,9 +1,11 @@
package migrator
import "github.com/portainer/portainer/api/dataservices"
import (
"github.com/portainer/portainer/api/dataservices"
)
func (m *Migrator) migrateDBVersionToDB34() error {
migrateLog.Info("Migrating stacks")
migrateLog.Info("- updating stacks")
err := MigrateStackEntryPoint(m.stackService)
if err != nil {
return err

View File

@@ -3,7 +3,7 @@ package migrator
func (m *Migrator) migrateDBVersionToDB35() error {
// These should have been migrated already, but due to an earlier bug and a bunch of duplicates,
// calling it again will now fix the issue as the function has been repaired.
migrateLog.Info("Updating dockerhub registries")
migrateLog.Info("- updating dockerhub registries")
err := m.updateDockerhubToDB32()
if err != nil {
return err

View File

@@ -0,0 +1,36 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
)
func (m *Migrator) migrateDBVersionToDB36() error {
migrateLog.Info("Updating user authorizations")
if err := m.migrateUsersToDB36(); err != nil {
return err
}
return nil
}
func (m *Migrator) migrateUsersToDB36() error {
users, err := m.userService.Users()
if err != nil {
return err
}
for _, user := range users {
currentAuthorizations := authorization.DefaultPortainerAuthorizations()
currentAuthorizations[portainer.OperationPortainerUserListToken] = true
currentAuthorizations[portainer.OperationPortainerUserCreateToken] = true
currentAuthorizations[portainer.OperationPortainerUserRevokeToken] = true
user.PortainerAuthorizations = currentAuthorizations
err = m.userService.UpdateUser(user.ID, &user)
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,31 @@
package migrator
import "github.com/portainer/portainer/api/internal/endpointutils"
func (m *Migrator) migrateDBVersionToDB40() error {
if err := m.trustCurrentEdgeEndpointsDB40(); err != nil {
return err
}
return nil
}
func (m *Migrator) trustCurrentEdgeEndpointsDB40() error {
migrateLog.Info("- trusting current edge endpoints")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
if endpointutils.IsEdgeEndpoint(&endpoint) {
endpoint.UserTrusted = true
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
"github.com/portainer/portainer/api/dataservices/role"
@@ -26,12 +27,12 @@ var migrateLog = plog.NewScopedLog("database, migrate")
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
currentDBVersion int
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
extensionService *extension.Service
fdoProfilesService *fdoprofile.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
@@ -54,6 +55,7 @@ type (
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
@@ -78,6 +80,7 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
extensionService: parameters.ExtensionService,
fdoProfilesService: parameters.FDOProfilesService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,

View File

@@ -17,6 +17,7 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
@@ -50,6 +51,7 @@ type Store struct {
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
HelmUserRepositoryService *helmuserrepository.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
@@ -129,6 +131,12 @@ func (store *Store) initServices() error {
}
store.ExtensionService = extensionService
fdoProfilesService, err := fdoprofile.NewService(store.connection)
if err != nil {
return err
}
store.FDOProfilesService = fdoProfilesService
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
if err != nil {
return err
@@ -257,6 +265,11 @@ func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
return store.EndpointRelationService
}
// FDOProfile gives access to the FDOProfile data management layer
func (store *Store) FDOProfile() dataservices.FDOProfileService {
return store.FDOProfilesService
}
// HelmUserRepository access the helm user repository settings
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
return store.HelmUserRepositoryService
@@ -356,6 +369,7 @@ type storeExport struct {
User []portainer.User `json:"users,omitempty"`
Version map[string]string `json:"version,omitempty"`
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
func (store *Store) Export(filename string) (err error) {
@@ -363,118 +377,184 @@ func (store *Store) Export(filename string) (err error) {
backup := storeExport{}
if c, err := store.CustomTemplate().CustomTemplates(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Custom Templates")
}
} else {
backup.CustomTemplate = c
}
if e, err := store.EdgeGroup().EdgeGroups(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Edge Groups")
}
} else {
backup.EdgeGroup = e
}
if e, err := store.EdgeJob().EdgeJobs(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Edge Jobs")
}
} else {
backup.EdgeJob = e
}
if e, err := store.EdgeStack().EdgeStacks(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Edge Stacks")
}
} else {
backup.EdgeStack = e
}
if e, err := store.Endpoint().Endpoints(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Endpoints")
}
} else {
backup.Endpoint = e
}
if e, err := store.EndpointGroup().EndpointGroups(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Endpoint Groups")
}
} else {
backup.EndpointGroup = e
}
if r, err := store.EndpointRelation().EndpointRelations(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Endpoint Relations")
}
} else {
backup.EndpointRelation = r
}
if r, err := store.ExtensionService.Extensions(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Extensions")
}
} else {
backup.Extensions = r
}
if r, err := store.HelmUserRepository().HelmUserRepositorys(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if r, err := store.HelmUserRepository().HelmUserRepositories(); err != nil {
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Helm User Repositories")
}
} else {
backup.HelmUserRepository = r
}
if r, err := store.Registry().Registries(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Registries")
}
} else {
backup.Registry = r
}
if c, err := store.ResourceControl().ResourceControls(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Resource Controls")
}
} else {
backup.ResourceControl = c
}
if role, err := store.Role().Roles(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Roles")
}
} else {
backup.Role = role
}
if r, err := store.ScheduleService.Schedules(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Schedules")
}
} else {
backup.Schedules = r
}
if settings, err := store.Settings().Settings(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Settings")
}
} else {
backup.Settings = *settings
}
if settings, err := store.SSLSettings().Settings(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting SSL Settings")
}
} else {
backup.SSLSettings = *settings
}
if t, err := store.Stack().Stacks(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Stacks")
}
} else {
backup.Stack = t
}
if t, err := store.Tag().Tags(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Tags")
}
} else {
backup.Tag = t
}
if t, err := store.TeamMembership().TeamMemberships(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Team Memberships")
}
} else {
backup.TeamMembership = t
}
if t, err := store.Team().Teams(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Teams")
}
} else {
backup.Team = t
}
if info, err := store.TunnelServer().Info(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Tunnel Server")
}
} else {
backup.TunnelServer = *info
}
if users, err := store.User().Users(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Users")
}
} else {
backup.User = users
}
if webhooks, err := store.Webhook().Webhooks(); err != nil {
logrus.WithError(err).Debugf("Export boom")
if !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting Webhooks")
}
} else {
backup.Webhook = webhooks
}
v, err := store.Version().DBVersion()
if err != nil {
logrus.WithError(err).Debugf("Export boom")
if err != nil && !store.IsErrObjectNotFound(err) {
logrus.WithError(err).Errorf("Exporting DB version")
}
instance, _ := store.Version().InstanceID()
backup.Version = map[string]string{
@@ -482,6 +562,11 @@ func (store *Store) Export(filename string) (err error) {
"INSTANCE_ID": instance,
}
backup.Metadata, err = store.connection.BackupMetadata()
if err != nil {
logrus.WithError(err).Errorf("Exporting Metadata")
}
b, err := json.MarshalIndent(backup, "", " ")
if err != nil {
return err
@@ -490,6 +575,7 @@ func (store *Store) Export(filename string) (err error) {
}
func (store *Store) Import(filename string) (err error) {
backup := storeExport{}
s, err := ioutil.ReadFile(filename)
@@ -518,50 +604,66 @@ func (store *Store) Import(filename string) (err error) {
for _, v := range backup.CustomTemplate {
store.CustomTemplate().UpdateCustomTemplate(v.ID, &v)
}
for _, v := range backup.EdgeGroup {
store.EdgeGroup().UpdateEdgeGroup(v.ID, &v)
}
for _, v := range backup.EdgeJob {
store.EdgeJob().UpdateEdgeJob(v.ID, &v)
}
for _, v := range backup.EdgeStack {
store.EdgeStack().UpdateEdgeStack(v.ID, &v)
}
for _, v := range backup.Endpoint {
store.Endpoint().UpdateEndpoint(v.ID, &v)
}
for _, v := range backup.EndpointGroup {
store.EndpointGroup().UpdateEndpointGroup(v.ID, &v)
}
for _, v := range backup.EndpointRelation {
store.EndpointRelation().UpdateEndpointRelation(v.EndpointID, &v)
}
for _, v := range backup.HelmUserRepository {
store.HelmUserRepository().UpdateHelmUserRepository(v.ID, &v)
}
for _, v := range backup.Registry {
store.Registry().UpdateRegistry(v.ID, &v)
}
for _, v := range backup.ResourceControl {
store.ResourceControl().UpdateResourceControl(v.ID, &v)
}
for _, v := range backup.Role {
store.Role().UpdateRole(v.ID, &v)
}
store.Settings().UpdateSettings(&backup.Settings)
store.SSLSettings().UpdateSettings(&backup.SSLSettings)
for _, v := range backup.Stack {
store.Stack().UpdateStack(v.ID, &v)
}
for _, v := range backup.Tag {
store.Tag().UpdateTag(v.ID, &v)
}
for _, v := range backup.TeamMembership {
store.TeamMembership().UpdateTeamMembership(v.ID, &v)
}
for _, v := range backup.Team {
store.Team().UpdateTeam(v.ID, &v)
}
store.TunnelServer().UpdateInfo(&backup.TunnelServer)
for _, user := range backup.User {
@@ -570,10 +672,9 @@ func (store *Store) Import(filename string) (err error) {
}
}
// backup[store.Webhook().BucketName()], err = store.Webhook().Webhooks()
// if err != nil {
// logrus.WithError(err).Debugf("Export boom")
// }
for _, v := range backup.Webhook {
store.Webhook().UpdateWebhook(v.ID, &v)
}
return nil
return store.connection.RestoreMetadata(backup.Metadata)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,808 @@
{
"dockerhub": [
{
"Authentication": false,
"Username": ""
}
],
"endpoint_groups": [
{
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"Description": "Unassigned endpoints",
"Id": 1,
"Labels": [],
"Name": "Unassigned",
"TagIds": [],
"Tags": null,
"TeamAccessPolicies": {},
"UserAccessPolicies": {}
}
],
"endpoint_relations": [
{
"EdgeStacks": {},
"EndpointID": 1
}
],
"endpoints": [
{
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"AzureCredentials": {
"ApplicationID": "",
"AuthenticationKey": "",
"TenantID": ""
},
"ComposeSyntaxMaxVersion": "",
"EdgeCheckinInterval": 0,
"EdgeKey": "",
"GroupId": 1,
"Id": 1,
"IsEdgeDevice": false,
"Kubernetes": {
"Configuration": {
"IngressClasses": null,
"RestrictDefaultNamespace": false,
"StorageClasses": null,
"UseLoadBalancer": false,
"UseServerMetrics": false
},
"Snapshots": null
},
"LastCheckInDate": 0,
"Name": "local",
"PublicURL": "",
"QueryDate": 0,
"SecuritySettings": {
"allowBindMountsForRegularUsers": true,
"allowContainerCapabilitiesForRegularUsers": true,
"allowDeviceMappingForRegularUsers": true,
"allowHostNamespaceForRegularUsers": true,
"allowPrivilegedModeForRegularUsers": true,
"allowStackManagementForRegularUsers": true,
"allowSysctlSettingForRegularUsers": false,
"allowVolumeBrowserForRegularUsers": false,
"enableHostManagementFeatures": false
},
"Snapshots": [
{
"DockerSnapshotRaw": {
"Containers": null,
"Images": null,
"Info": null,
"Networks": null,
"Version": null,
"Volumes": null
},
"DockerVersion": "20.10.13",
"HealthyContainerCount": 0,
"ImageCount": 9,
"NodeCount": 0,
"RunningContainerCount": 5,
"ServiceCount": 0,
"StackCount": 2,
"StoppedContainerCount": 0,
"Swarm": false,
"Time": 1648610112,
"TotalCPU": 8,
"TotalMemory": 25098706944,
"UnhealthyContainerCount": 0,
"VolumeCount": 10
}
],
"Status": 1,
"TLSConfig": {
"TLS": false,
"TLSSkipVerify": false
},
"TagIds": [],
"Tags": null,
"TeamAccessPolicies": {},
"Type": 1,
"URL": "unix:///var/run/docker.sock",
"UserAccessPolicies": {},
"UserTrusted": false
}
],
"registries": [
{
"Authentication": true,
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"BaseURL": "",
"Ecr": {
"Region": ""
},
"Gitlab": {
"InstanceURL": "",
"ProjectId": 0,
"ProjectPath": ""
},
"Id": 1,
"ManagementConfiguration": null,
"Name": "canister.io",
"Password": "MjWbx8A6YK7cw7",
"Quay": {
"OrganisationName": "",
"UseOrganisation": false
},
"RegistryAccesses": {
"1": {
"Namespaces": [],
"TeamAccessPolicies": {},
"UserAccessPolicies": {}
}
},
"TeamAccessPolicies": {},
"Type": 3,
"URL": "cloud.canister.io:5000",
"UserAccessPolicies": {},
"Username": "prabhatkhera"
}
],
"resource_control": [
{
"AdministratorsOnly": false,
"Id": 2,
"Public": true,
"ResourceId": "762gbwaj8r4gcsdy8ld1u4why",
"SubResourceIds": [],
"System": false,
"TeamAccesses": [],
"Type": 5,
"UserAccesses": []
},
{
"AdministratorsOnly": false,
"Id": 3,
"Public": true,
"ResourceId": "1_alpine",
"SubResourceIds": [],
"System": false,
"TeamAccesses": [],
"Type": 6,
"UserAccesses": []
},
{
"AdministratorsOnly": false,
"Id": 4,
"Public": true,
"ResourceId": "1_redis",
"SubResourceIds": [],
"System": false,
"TeamAccesses": [],
"Type": 6,
"UserAccesses": []
},
{
"AdministratorsOnly": false,
"Id": 5,
"Public": false,
"ResourceId": "1_nginx",
"SubResourceIds": [],
"System": false,
"TeamAccesses": [
{
"AccessLevel": 1,
"TeamId": 1
}
],
"Type": 6,
"UserAccesses": []
}
],
"roles": [
{
"Authorizations": {
"DockerAgentBrowseDelete": true,
"DockerAgentBrowseGet": true,
"DockerAgentBrowseList": true,
"DockerAgentBrowsePut": true,
"DockerAgentBrowseRename": true,
"DockerAgentHostInfo": true,
"DockerAgentList": true,
"DockerAgentPing": true,
"DockerAgentUndefined": true,
"DockerBuildCancel": true,
"DockerBuildPrune": true,
"DockerConfigCreate": true,
"DockerConfigDelete": true,
"DockerConfigInspect": true,
"DockerConfigList": true,
"DockerConfigUpdate": true,
"DockerContainerArchive": true,
"DockerContainerArchiveInfo": true,
"DockerContainerAttach": true,
"DockerContainerAttachWebsocket": true,
"DockerContainerChanges": true,
"DockerContainerCreate": true,
"DockerContainerDelete": true,
"DockerContainerExec": true,
"DockerContainerExport": true,
"DockerContainerInspect": true,
"DockerContainerKill": true,
"DockerContainerList": true,
"DockerContainerLogs": true,
"DockerContainerPause": true,
"DockerContainerPrune": true,
"DockerContainerPutContainerArchive": true,
"DockerContainerRename": true,
"DockerContainerResize": true,
"DockerContainerRestart": true,
"DockerContainerStart": true,
"DockerContainerStats": true,
"DockerContainerStop": true,
"DockerContainerTop": true,
"DockerContainerUnpause": true,
"DockerContainerUpdate": true,
"DockerContainerWait": true,
"DockerDistributionInspect": true,
"DockerEvents": true,
"DockerExecInspect": true,
"DockerExecResize": true,
"DockerExecStart": true,
"DockerImageBuild": true,
"DockerImageCommit": true,
"DockerImageCreate": true,
"DockerImageDelete": true,
"DockerImageGet": true,
"DockerImageGetAll": true,
"DockerImageHistory": true,
"DockerImageInspect": true,
"DockerImageList": true,
"DockerImageLoad": true,
"DockerImagePrune": true,
"DockerImagePush": true,
"DockerImageSearch": true,
"DockerImageTag": true,
"DockerInfo": true,
"DockerNetworkConnect": true,
"DockerNetworkCreate": true,
"DockerNetworkDelete": true,
"DockerNetworkDisconnect": true,
"DockerNetworkInspect": true,
"DockerNetworkList": true,
"DockerNetworkPrune": true,
"DockerNodeDelete": true,
"DockerNodeInspect": true,
"DockerNodeList": true,
"DockerNodeUpdate": true,
"DockerPing": true,
"DockerPluginCreate": true,
"DockerPluginDelete": true,
"DockerPluginDisable": true,
"DockerPluginEnable": true,
"DockerPluginInspect": true,
"DockerPluginList": true,
"DockerPluginPrivileges": true,
"DockerPluginPull": true,
"DockerPluginPush": true,
"DockerPluginSet": true,
"DockerPluginUpgrade": true,
"DockerSecretCreate": true,
"DockerSecretDelete": true,
"DockerSecretInspect": true,
"DockerSecretList": true,
"DockerSecretUpdate": true,
"DockerServiceCreate": true,
"DockerServiceDelete": true,
"DockerServiceInspect": true,
"DockerServiceList": true,
"DockerServiceLogs": true,
"DockerServiceUpdate": true,
"DockerSessionStart": true,
"DockerSwarmInit": true,
"DockerSwarmInspect": true,
"DockerSwarmJoin": true,
"DockerSwarmLeave": true,
"DockerSwarmUnlock": true,
"DockerSwarmUnlockKey": true,
"DockerSwarmUpdate": true,
"DockerSystem": true,
"DockerTaskInspect": true,
"DockerTaskList": true,
"DockerTaskLogs": true,
"DockerUndefined": true,
"DockerVersion": true,
"DockerVolumeCreate": true,
"DockerVolumeDelete": true,
"DockerVolumeInspect": true,
"DockerVolumeList": true,
"DockerVolumePrune": true,
"EndpointResourcesAccess": true,
"IntegrationStoridgeAdmin": true,
"PortainerResourceControlCreate": true,
"PortainerResourceControlUpdate": true,
"PortainerStackCreate": true,
"PortainerStackDelete": true,
"PortainerStackFile": true,
"PortainerStackInspect": true,
"PortainerStackList": true,
"PortainerStackMigrate": true,
"PortainerStackUpdate": true,
"PortainerWebhookCreate": true,
"PortainerWebhookDelete": true,
"PortainerWebhookList": true,
"PortainerWebsocketExec": true
},
"Description": "Full control of all resources in an endpoint",
"Id": 1,
"Name": "Endpoint administrator",
"Priority": 1
},
{
"Authorizations": {
"DockerAgentHostInfo": true,
"DockerAgentList": true,
"DockerAgentPing": true,
"DockerConfigInspect": true,
"DockerConfigList": true,
"DockerContainerArchiveInfo": true,
"DockerContainerChanges": true,
"DockerContainerInspect": true,
"DockerContainerList": true,
"DockerContainerLogs": true,
"DockerContainerStats": true,
"DockerContainerTop": true,
"DockerDistributionInspect": true,
"DockerEvents": true,
"DockerImageGet": true,
"DockerImageGetAll": true,
"DockerImageHistory": true,
"DockerImageInspect": true,
"DockerImageList": true,
"DockerImageSearch": true,
"DockerInfo": true,
"DockerNetworkInspect": true,
"DockerNetworkList": true,
"DockerNodeInspect": true,
"DockerNodeList": true,
"DockerPing": true,
"DockerPluginList": true,
"DockerSecretInspect": true,
"DockerSecretList": true,
"DockerServiceInspect": true,
"DockerServiceList": true,
"DockerServiceLogs": true,
"DockerSwarmInspect": true,
"DockerSystem": true,
"DockerTaskInspect": true,
"DockerTaskList": true,
"DockerTaskLogs": true,
"DockerVersion": true,
"DockerVolumeInspect": true,
"DockerVolumeList": true,
"EndpointResourcesAccess": true,
"PortainerStackFile": true,
"PortainerStackInspect": true,
"PortainerStackList": true,
"PortainerWebhookList": true
},
"Description": "Read-only access of all resources in an endpoint",
"Id": 2,
"Name": "Helpdesk",
"Priority": 2
},
{
"Authorizations": {
"DockerAgentHostInfo": true,
"DockerAgentList": true,
"DockerAgentPing": true,
"DockerAgentUndefined": true,
"DockerBuildCancel": true,
"DockerBuildPrune": true,
"DockerConfigCreate": true,
"DockerConfigDelete": true,
"DockerConfigInspect": true,
"DockerConfigList": true,
"DockerConfigUpdate": true,
"DockerContainerArchive": true,
"DockerContainerArchiveInfo": true,
"DockerContainerAttach": true,
"DockerContainerAttachWebsocket": true,
"DockerContainerChanges": true,
"DockerContainerCreate": true,
"DockerContainerDelete": true,
"DockerContainerExec": true,
"DockerContainerExport": true,
"DockerContainerInspect": true,
"DockerContainerKill": true,
"DockerContainerList": true,
"DockerContainerLogs": true,
"DockerContainerPause": true,
"DockerContainerPutContainerArchive": true,
"DockerContainerRename": true,
"DockerContainerResize": true,
"DockerContainerRestart": true,
"DockerContainerStart": true,
"DockerContainerStats": true,
"DockerContainerStop": true,
"DockerContainerTop": true,
"DockerContainerUnpause": true,
"DockerContainerUpdate": true,
"DockerContainerWait": true,
"DockerDistributionInspect": true,
"DockerEvents": true,
"DockerExecInspect": true,
"DockerExecResize": true,
"DockerExecStart": true,
"DockerImageBuild": true,
"DockerImageCommit": true,
"DockerImageCreate": true,
"DockerImageDelete": true,
"DockerImageGet": true,
"DockerImageGetAll": true,
"DockerImageHistory": true,
"DockerImageInspect": true,
"DockerImageList": true,
"DockerImageLoad": true,
"DockerImagePush": true,
"DockerImageSearch": true,
"DockerImageTag": true,
"DockerInfo": true,
"DockerNetworkConnect": true,
"DockerNetworkCreate": true,
"DockerNetworkDelete": true,
"DockerNetworkDisconnect": true,
"DockerNetworkInspect": true,
"DockerNetworkList": true,
"DockerNodeDelete": true,
"DockerNodeInspect": true,
"DockerNodeList": true,
"DockerNodeUpdate": true,
"DockerPing": true,
"DockerPluginCreate": true,
"DockerPluginDelete": true,
"DockerPluginDisable": true,
"DockerPluginEnable": true,
"DockerPluginInspect": true,
"DockerPluginList": true,
"DockerPluginPrivileges": true,
"DockerPluginPull": true,
"DockerPluginPush": true,
"DockerPluginSet": true,
"DockerPluginUpgrade": true,
"DockerSecretCreate": true,
"DockerSecretDelete": true,
"DockerSecretInspect": true,
"DockerSecretList": true,
"DockerSecretUpdate": true,
"DockerServiceCreate": true,
"DockerServiceDelete": true,
"DockerServiceInspect": true,
"DockerServiceList": true,
"DockerServiceLogs": true,
"DockerServiceUpdate": true,
"DockerSessionStart": true,
"DockerSwarmInit": true,
"DockerSwarmInspect": true,
"DockerSwarmJoin": true,
"DockerSwarmLeave": true,
"DockerSwarmUnlock": true,
"DockerSwarmUnlockKey": true,
"DockerSwarmUpdate": true,
"DockerSystem": true,
"DockerTaskInspect": true,
"DockerTaskList": true,
"DockerTaskLogs": true,
"DockerUndefined": true,
"DockerVersion": true,
"DockerVolumeCreate": true,
"DockerVolumeDelete": true,
"DockerVolumeInspect": true,
"DockerVolumeList": true,
"PortainerResourceControlUpdate": true,
"PortainerStackCreate": true,
"PortainerStackDelete": true,
"PortainerStackFile": true,
"PortainerStackInspect": true,
"PortainerStackList": true,
"PortainerStackMigrate": true,
"PortainerStackUpdate": true,
"PortainerWebhookCreate": true,
"PortainerWebhookList": true,
"PortainerWebsocketExec": true
},
"Description": "Full control of assigned resources in an endpoint",
"Id": 3,
"Name": "Standard user",
"Priority": 3
},
{
"Authorizations": {
"DockerAgentHostInfo": true,
"DockerAgentList": true,
"DockerAgentPing": true,
"DockerConfigInspect": true,
"DockerConfigList": true,
"DockerContainerArchiveInfo": true,
"DockerContainerChanges": true,
"DockerContainerInspect": true,
"DockerContainerList": true,
"DockerContainerLogs": true,
"DockerContainerStats": true,
"DockerContainerTop": true,
"DockerDistributionInspect": true,
"DockerEvents": true,
"DockerImageGet": true,
"DockerImageGetAll": true,
"DockerImageHistory": true,
"DockerImageInspect": true,
"DockerImageList": true,
"DockerImageSearch": true,
"DockerInfo": true,
"DockerNetworkInspect": true,
"DockerNetworkList": true,
"DockerNodeInspect": true,
"DockerNodeList": true,
"DockerPing": true,
"DockerPluginList": true,
"DockerSecretInspect": true,
"DockerSecretList": true,
"DockerServiceInspect": true,
"DockerServiceList": true,
"DockerServiceLogs": true,
"DockerSwarmInspect": true,
"DockerSystem": true,
"DockerTaskInspect": true,
"DockerTaskList": true,
"DockerTaskLogs": true,
"DockerVersion": true,
"DockerVolumeInspect": true,
"DockerVolumeList": true,
"PortainerStackFile": true,
"PortainerStackInspect": true,
"PortainerStackList": true,
"PortainerWebhookList": true
},
"Description": "Read-only access of assigned resources in an endpoint",
"Id": 4,
"Name": "Read-only user",
"Priority": 4
}
],
"schedules": [
{
"Created": 1648608136,
"CronExpression": "@every 5m",
"EdgeSchedule": null,
"EndpointSyncJob": null,
"Id": 1,
"JobType": 2,
"Name": "system_snapshot",
"Recurring": true,
"ScriptExecutionJob": null,
"SnapshotJob": {}
}
],
"settings": {
"AgentSecret": "",
"AllowBindMountsForRegularUsers": true,
"AllowContainerCapabilitiesForRegularUsers": true,
"AllowDeviceMappingForRegularUsers": true,
"AllowHostNamespaceForRegularUsers": true,
"AllowPrivilegedModeForRegularUsers": true,
"AllowStackManagementForRegularUsers": true,
"AllowVolumeBrowserForRegularUsers": false,
"AuthenticationMethod": 1,
"BlackListedLabels": [],
"DisplayDonationHeader": false,
"DisplayExternalContributors": false,
"EdgeAgentCheckinInterval": 5,
"EdgePortainerUrl": "",
"EnableEdgeComputeFeatures": false,
"EnableHostManagementFeatures": false,
"EnableTelemetry": true,
"EnforceEdgeID": false,
"FeatureFlagSettings": null,
"HelmRepositoryURL": "https://charts.bitnami.com/bitnami",
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
"GroupSearchSettings": [
{
"GroupAttribute": "",
"GroupBaseDN": "",
"GroupFilter": ""
}
],
"ReaderDN": "",
"SearchSettings": [
{
"BaseDN": "",
"Filter": "",
"UserNameAttribute": ""
}
],
"StartTLS": false,
"TLSConfig": {
"TLS": false,
"TLSSkipVerify": false
},
"URL": ""
},
"LogoURL": "",
"OAuthSettings": {
"AccessTokenURI": "",
"AuthorizationURI": "",
"ClientID": "",
"DefaultTeamID": 0,
"KubeSecretKey": null,
"LogoutURI": "",
"OAuthAutoCreateUsers": false,
"RedirectURI": "",
"ResourceURI": "",
"SSO": false,
"Scopes": "",
"UserIdentifier": ""
},
"SnapshotInterval": "5m",
"TemplatesURL": "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json",
"TrustOnFirstConnect": false,
"UserSessionTimeout": "8h",
"fdoConfiguration": {
"enabled": false,
"ownerPassword": "",
"ownerURL": "",
"ownerUsername": ""
},
"openAMTConfiguration": {
"certFileContent": "",
"certFileName": "",
"certFilePassword": "",
"domainName": "",
"enabled": false,
"mpsPassword": "",
"mpsServer": "",
"mpsToken": "",
"mpsUser": ""
}
},
"ssl": {
"certPath": "",
"httpEnabled": true,
"keyPath": "",
"selfSigned": false
},
"stacks": [
{
"AdditionalFiles": null,
"AutoUpdate": null,
"CreatedBy": "",
"CreationDate": 0,
"EndpointId": 1,
"EntryPoint": "docker/alpine37-compose.yml",
"Env": [],
"FromAppTemplate": false,
"GitConfig": null,
"Id": 2,
"IsComposeFormat": false,
"Name": "alpine",
"Namespace": "",
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/2",
"ResourceControl": null,
"Status": 1,
"SwarmId": "s3fd604zdba7z13tbq2x6lyue",
"Type": 1,
"UpdateDate": 0,
"UpdatedBy": ""
},
{
"AdditionalFiles": null,
"AutoUpdate": null,
"CreatedBy": "",
"CreationDate": 0,
"EndpointId": 1,
"EntryPoint": "docker-compose.yml",
"Env": [],
"FromAppTemplate": false,
"GitConfig": null,
"Id": 5,
"IsComposeFormat": false,
"Name": "redis",
"Namespace": "",
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/5",
"ResourceControl": null,
"Status": 1,
"SwarmId": "",
"Type": 2,
"UpdateDate": 0,
"UpdatedBy": ""
},
{
"AdditionalFiles": null,
"AutoUpdate": null,
"CreatedBy": "",
"CreationDate": 0,
"EndpointId": 1,
"EntryPoint": "docker-compose.yml",
"Env": [],
"FromAppTemplate": false,
"GitConfig": null,
"Id": 6,
"IsComposeFormat": false,
"Name": "nginx",
"Namespace": "",
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/6",
"ResourceControl": null,
"Status": 1,
"SwarmId": "",
"Type": 2,
"UpdateDate": 0,
"UpdatedBy": ""
}
],
"teams": [
{
"Id": 1,
"Name": "hello"
}
],
"tunnel_server": {
"PrivateKeySeed": "IvX6ZPRuWtLS5zyg"
},
"users": [
{
"EndpointAuthorizations": null,
"Id": 1,
"Password": "$2a$10$siRDprr/5uUFAU8iom3Sr./WXQkN2dhSNjAC471pkJaALkghS762a",
"PortainerAuthorizations": {
"PortainerDockerHubInspect": true,
"PortainerEndpointGroupList": true,
"PortainerEndpointInspect": true,
"PortainerEndpointList": true,
"PortainerMOTD": true,
"PortainerRegistryInspect": true,
"PortainerRegistryList": true,
"PortainerTeamList": true,
"PortainerTemplateInspect": true,
"PortainerTemplateList": true,
"PortainerUserCreateToken": true,
"PortainerUserInspect": true,
"PortainerUserList": true,
"PortainerUserListToken": true,
"PortainerUserMemberships": true,
"PortainerUserRevokeToken": true
},
"Role": 1,
"TokenIssueAt": 0,
"UserTheme": "",
"Username": "admin"
},
{
"EndpointAuthorizations": null,
"Id": 2,
"Password": "$2a$10$WpCAW8mSt6FRRp1GkynbFOGSZnHR6E5j9cETZ8HiMlw06hVlDW/Li",
"PortainerAuthorizations": {
"PortainerDockerHubInspect": true,
"PortainerEndpointGroupList": true,
"PortainerEndpointInspect": true,
"PortainerEndpointList": true,
"PortainerMOTD": true,
"PortainerRegistryInspect": true,
"PortainerRegistryList": true,
"PortainerTeamList": true,
"PortainerTemplateInspect": true,
"PortainerTemplateList": true,
"PortainerUserCreateToken": true,
"PortainerUserInspect": true,
"PortainerUserList": true,
"PortainerUserListToken": true,
"PortainerUserMemberships": true,
"PortainerUserRevokeToken": true
},
"Role": 1,
"TokenIssueAt": 0,
"UserTheme": "",
"Username": "prabhat"
}
],
"version": {
"DB_UPDATING": "false",
"DB_VERSION": "35",
"INSTANCE_ID": "null"
}
}

View File

@@ -18,8 +18,8 @@ func (store *Store) GetConnection() portainer.Connection {
return store.connection
}
func MustNewTestStore(init bool) (bool, *Store, func()) {
newStore, store, teardown, err := NewTestStore(init)
func MustNewTestStore(init, secure bool) (bool, *Store, func()) {
newStore, store, teardown, err := NewTestStore(init, secure)
if err != nil {
if !errors.Is(err, errTempDir) {
teardown()
@@ -30,7 +30,7 @@ func MustNewTestStore(init bool) (bool, *Store, func()) {
return newStore, store, teardown
}
func NewTestStore(init bool) (bool, *Store, func(), error) {
func NewTestStore(init, secure bool) (bool, *Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner.
storePath, err := ioutil.TempDir("", "test-store")
if err != nil {
@@ -42,7 +42,12 @@ func NewTestStore(init bool) (bool, *Store, func(), error) {
return false, nil, nil, err
}
connection, err := database.NewDatabase("boltdb", storePath)
secretKey := []byte("apassphrasewhichneedstobe32bytes")
if !secure {
secretKey = nil
}
connection, err := database.NewDatabase("boltdb", storePath, secretKey)
if err != nil {
panic(err)
}

View File

@@ -0,0 +1,15 @@
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

@@ -0,0 +1,61 @@
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

@@ -0,0 +1,17 @@
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

@@ -33,7 +33,7 @@ func (s StringSet) List() []string {
list := make([]string, s.Len())
i := 0
for k, _ := range s {
for k := range s {
list[i] = k
i++
}

View File

@@ -46,7 +46,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
}
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint, forceRereate bool) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to fetch environment proxy")
@@ -62,7 +62,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath, forceRereate)
return errors.Wrap(err, "failed to deploy a stack")
}

View File

@@ -50,7 +50,7 @@ func Test_UpAndDown(t *testing.T) {
ctx := context.TODO()
err = w.Up(ctx, stack, endpoint)
err = w.Up(ctx, stack, endpoint, false)
if err != nil {
t.Fatalf("Error calling docker-compose up: %s", err)
}

View File

@@ -3,6 +3,7 @@ package exec
import (
"bytes"
"fmt"
"os"
"os/exec"
"path"
"runtime"
@@ -123,6 +124,8 @@ func (deployer *KubernetesDeployer) command(operation string, userID portainer.U
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()

View File

@@ -35,6 +35,8 @@ const (
ManifestFileDefaultName = "k8s-deployment.yml"
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
EdgeStackStorePath = "edge_stacks"
// FDOProfileStorePath represents the subfolder where FDO profiles files are stored in the file store folder.
FDOProfileStorePath = "fdo_profiles"
// PrivateKeyFile represents the name on disk of the file containing the private key.
PrivateKeyFile = "portainer.key"
// PublicKeyFile represents the name on disk of the file containing the public key.
@@ -54,10 +56,12 @@ const (
TempPath = "tmp"
// SSLCertPath represents the default ssl certificates path
SSLCertPath = "certs"
// DefaultSSLCertFilename represents the default ssl certificate file name
DefaultSSLCertFilename = "cert.pem"
// DefaultSSLKeyFilename represents the default ssl key file name
DefaultSSLKeyFilename = "key.pem"
// SSLCertFilename represents the ssl certificate file name
SSLCertFilename = "cert.pem"
// SSLKeyFilename represents the ssl key file name
SSLKeyFilename = "key.pem"
// SSLCACertFilename represents the CA ssl certificate file name for mTLS
SSLCACertFilename = "ca-cert.pem"
)
// ErrUndefinedTLSFileType represents an error returned on undefined TLS file type
@@ -159,7 +163,7 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
}
if !exists {
return errors.New("File doesn't exist")
return errors.New(fmt.Sprintf("File (%s) doesn't exist", fromFilePath))
}
finput, err := os.Open(fromFilePath)
@@ -578,8 +582,8 @@ func (service *Service) wrapFileStore(filepath string) string {
}
func defaultCertPathUnderFileStore() (string, string) {
certPath := JoinPaths(SSLCertPath, DefaultSSLCertFilename)
keyPath := JoinPaths(SSLCertPath, DefaultSSLKeyFilename)
certPath := JoinPaths(SSLCertPath, SSLCertFilename)
keyPath := JoinPaths(SSLCertPath, SSLKeyFilename)
return certPath, keyPath
}
@@ -625,6 +629,18 @@ func (service *Service) CopySSLCertPair(certPath, keyPath string) (string, strin
return defCertPath, defKeyPath, nil
}
// CopySSLCACert copies the specified caCert pem file
func (service *Service) CopySSLCACert(caCertPath string) (string, error) {
toFilePath := service.wrapFileStore(JoinPaths(SSLCertPath, SSLCACertFilename))
err := service.Copy(caCertPath, toFilePath, true)
if err != nil {
return "", err
}
return toFilePath, nil
}
// FileExists checks for the existence of the specified file.
func FileExists(filePath string) (bool, error) {
if _, err := os.Stat(filePath); err != nil {
@@ -652,3 +668,20 @@ func MoveDirectory(originalPath, newPath string) error {
return os.Rename(originalPath, newPath)
}
// StoreFDOProfileFileFromBytes creates a subfolder in the FDOProfileStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error) {
err := service.createDirectoryInStore(FDOProfileStorePath)
if err != nil {
return "", err
}
filePath := JoinPaths(FDOProfileStorePath, fdoProfileIdentifier)
err = service.createFileInStore(filePath, bytes.NewReader(data))
if err != nil {
return "", err
}
return service.wrapFileStore(filePath), nil
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/pkg/errors"
)
// WriteToFile creates a file in the filesystem storage
func WriteToFile(dst string, content []byte) error {
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
return errors.Wrapf(err, "failed to create filestructure for the path %q", dst)

View File

@@ -3,49 +3,53 @@ module github.com/portainer/portainer/api
go 1.17
require (
github.com/Microsoft/go-winio v0.4.17
github.com/Microsoft/go-winio v0.5.1
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/aws/aws-sdk-go-v2 v1.11.1
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1
github.com/boltdb/bolt v1.3.1
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/cli v20.10.9+incompatible
github.com/docker/docker v20.10.9+incompatible
github.com/fvbommel/sortorder v1.0.2
github.com/fxamacker/cbor/v2 v2.3.0
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
github.com/go-git/go-git/v5 v5.3.0
github.com/go-ldap/ldap/v3 v3.1.8
github.com/go-playground/validator/v10 v10.10.1
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-cmp v0.5.6
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.4.2
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/golang-lru v0.5.4
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.11
github.com/json-iterator/go v1.1.12
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/docker-compose-wrapper v0.0.0-20220407011010-3c7408969ad3
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
github.com/viney-shih/go-lock v1.1.1
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
k8s.io/client-go v0.22.2
k8s.io/api v0.22.5
k8s.io/apimachinery v0.22.5
k8s.io/client-go v0.22.5
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
)
@@ -57,64 +61,66 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/containerd/containerd v1.5.7 // indirect
github.com/containerd/containerd v1.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/evanphx/json-patch v4.11.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.3.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.1.0 // indirect
github.com/go-logr/logr v0.4.0 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jpillora/ansi v0.0.0-20170202005112-f496b27cd669 // indirect
github.com/jpillora/requestlog v0.0.0-20181015073026-df8817be5f82 // indirect
github.com/jpillora/sizestr v0.0.0-20160130011556-e2ea2fa42fb9 // indirect
github.com/jpillora/ansi v1.0.2 // indirect
github.com/jpillora/requestlog v1.0.0 // indirect
github.com/jpillora/sizestr v1.0.0 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 // indirect
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect
golang.org/x/text v0.3.6 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.5 // indirect
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect
google.golang.org/grpc v1.33.2 // indirect
google.golang.org/protobuf v1.26.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.9.0 // indirect
k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e // indirect
k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"golang.org/x/sync/errgroup"
)
@@ -32,7 +31,7 @@ type Service struct {
}
// NewService initializes a new service.
func NewService(dataStore dataservices.DataStore) *Service {
func NewService() *Service {
return &Service{
httpsClient: &http.Client{
Timeout: httpClientTimeout,

View File

@@ -12,6 +12,8 @@ import (
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/passwordutils"
)
type authenticatePayload struct {
@@ -49,7 +51,7 @@ func (payload *authenticatePayload) Validate(r *http.Request) error {
// @failure 422 "Invalid Credentials"
// @failure 500 "Server error"
// @router /auth [post]
func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
func (handler *Handler) authenticate(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload authenticatePayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -61,39 +63,36 @@ func (handler *Handler) authenticate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
u, err := handler.DataStore.User().UserByUsername(payload.Username)
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
user, err := handler.DataStore.User().UserByUsername(payload.Username)
if err != nil {
if !handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
if settings.AuthenticationMethod == portainer.AuthenticationInternal ||
settings.AuthenticationMethod == portainer.AuthenticationOAuth ||
(settings.AuthenticationMethod == portainer.AuthenticationLDAP && !settings.LDAPSettings.AutoCreateUsers) {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
}
if handler.DataStore.IsErrObjectNotFound(err) && (settings.AuthenticationMethod == portainer.AuthenticationInternal || settings.AuthenticationMethod == portainer.AuthenticationOAuth) {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
if user != nil && isUserInitialAdmin(user) || settings.AuthenticationMethod == portainer.AuthenticationInternal {
return handler.authenticateInternal(rw, user, payload.Password)
}
if settings.AuthenticationMethod == portainer.AuthenticationOAuth {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Only initial admin is allowed to login without oauth", httperrors.ErrUnauthorized}
}
if settings.AuthenticationMethod == portainer.AuthenticationLDAP {
if u == nil && settings.LDAPSettings.AutoCreateUsers {
return handler.authenticateLDAPAndCreateUser(w, payload.Username, payload.Password, &settings.LDAPSettings)
} else if u == nil && !settings.LDAPSettings.AutoCreateUsers {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
return handler.authenticateLDAP(w, u, payload.Password, &settings.LDAPSettings)
return handler.authenticateLDAP(rw, user, payload.Username, payload.Password, &settings.LDAPSettings)
}
return handler.authenticateInternal(w, u, payload.Password)
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Login method is not supported", httperrors.ErrUnauthorized}
}
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
err := handler.LDAPService.AuthenticateUser(user.Username, password, ldapSettings)
if err != nil {
return handler.authenticateInternal(w, user, password)
}
err = handler.addUserIntoTeams(user, ldapSettings)
if err != nil {
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
return handler.writeToken(w, user)
func isUserInitialAdmin(user *portainer.User) bool {
return int(user.ID) == 1
}
func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portainer.User, password string) *httperror.HandlerError {
@@ -102,23 +101,31 @@ func (handler *Handler) authenticateInternal(w http.ResponseWriter, user *portai
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", httperrors.ErrUnauthorized}
}
return handler.writeToken(w, user)
forceChangePassword := !passwordutils.StrengthCheck(password)
return handler.writeToken(w, user, forceChangePassword)
}
func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.User, username, password string, ldapSettings *portainer.LDAPSettings) *httperror.HandlerError {
err := handler.LDAPService.AuthenticateUser(username, password, ldapSettings)
if err != nil {
return &httperror.HandlerError{http.StatusUnprocessableEntity, "Invalid credentials", err}
return &httperror.HandlerError{
StatusCode: http.StatusForbidden,
Message: "Only initial admin is allowed to login without oauth",
Err: err,
}
}
user := &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
}
if user == nil {
user = &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
}
err = handler.DataStore.User().Create(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
err = handler.DataStore.User().Create(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
}
err = handler.addUserIntoTeams(user, ldapSettings)
@@ -126,11 +133,13 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
}
return handler.writeToken(w, user)
return handler.writeToken(w, user, false)
}
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
return handler.persistAndWriteToken(w, composeTokenData(user))
func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User, forceChangePassword bool) *httperror.HandlerError {
tokenData := composeTokenData(user, forceChangePassword)
return handler.persistAndWriteToken(w, tokenData)
}
func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError {
@@ -199,10 +208,11 @@ func teamMembershipExists(teamID portainer.TeamID, memberships []portainer.TeamM
return false
}
func composeTokenData(user *portainer.User) *portainer.TokenData {
func composeTokenData(user *portainer.User, forceChangePassword bool) *portainer.TokenData {
return &portainer.TokenData{
ID: user.ID,
Username: user.Username,
Role: user.Role,
ID: user.ID,
Username: user.Username,
Role: user.Role,
ForceChangePassword: forceChangePassword,
}
}

View File

@@ -110,5 +110,5 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
}
return handler.writeToken(w, user)
return handler.writeToken(w, user, false)
}

View File

@@ -4,6 +4,7 @@ import (
"errors"
"log"
"net/http"
"os"
"regexp"
"strconv"
@@ -271,14 +272,19 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
if err != nil {
return nil, err
}
isValidProject := true
defer func() {
if !isValidProject {
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
}
}
}()
entryPath := filesystem.JoinPaths(projectPath, customTemplate.EntryPoint)
exists, err := handler.FileService.FileExists(entryPath)
if err != nil || !exists {
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
}
isValidProject = false
}
if err != nil {
@@ -289,6 +295,16 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
return nil, errors.New("Invalid Compose file, ensure that the Compose file path is correct")
}
info, err := os.Lstat(entryPath)
if err != nil {
isValidProject = false
return nil, err
}
if info.Mode()&os.ModeSymlink != 0 { // entry is a symlink
isValidProject = false
return nil, errors.New("Invalid Compose file, ensure that the Compose file is not a symbolic link")
}
return customTemplate, nil
}

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