Compare commits

..

654 Commits

Author SHA1 Message Date
Matt Hook
d3d4361850 bump version to 2.16.1 (#8018) 2022-11-09 14:28:40 +13:00
andres-portainer
1b237151a9 fix(snapshots): remove snapshots when removing endpoints EE-4527 (#7971)
* fix(snapshots): remove snapshots when removing endpoints EE-4527

* Fix nil pointer dereference.

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2022-11-07 20:28:06 -03:00
Chaim Lev-Ari
0ddf31f3e2 fix(stack): validate original containers names [EE-4520] (#7977) 2022-11-06 10:40:30 +02:00
LP B
abd513801f fix(app/logs): change pattern to detect double serialized JSON logs [EE-4525] (#7961)
* fix(app/logs): change pattern to detect double serialized JSON logs

* fix(app/logs): fallback to raw display when parsing fails + include timestamp for Zerolog logs
2022-11-04 13:58:05 +01:00
Ali
354eb8c3c0 fix(slider): use and update react slider EE-4522 (#7988) 2022-11-04 14:13:02 +13:00
congs
539e7fe422 fix(registry): EE-4526 Registry Manage access link broken (#7976) 2022-11-04 12:10:32 +13:00
Ali
11e42f54ba fix(app): fix external app edit EE-4529 (#7964) 2022-11-03 13:04:53 +13:00
Dakota Walsh
db1d56621e fix(ingresses): migrate to new allow/disallow format EE-4465 (#7894) 2022-10-28 16:27:01 +13:00
Dmitry Salakhov
4f173d0fd2 fix(image): build image from file (#7928) [EE-4501] 2022-10-27 23:31:42 +13:00
Dakota Walsh
60183be3fe fix(ingress): allow none controller type EE-4420 (#7884)
Co-authored-by: testA113 <alex.harris@portainer.io>
2022-10-25 08:12:33 +13:00
Chaim Lev-Ari
ae467a9754 chore(edge): add aria-label for edge-group selector [EE-4466] (#7895)
* chore(edge): add aria-label for edge-group selector

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

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

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

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

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

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

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2022-10-20 16:34:01 +02:00
Prabhat Khera
d27df1d4cd fix volume claims with k8s app (#7900) 2022-10-20 15:43:24 +13:00
itsconquest
7343a2ebd7 fix(UAC): put team into resource control when editing as team lead [EE-4457] (#7885)
* fix(UAC): put team into resource control when editing as team lead [EE-4457]

* populate form values & payload correctly
2022-10-20 10:18:51 +13:00
itsconquest
71d2473070 fix(notifications): sort by newest first by default [EE-4467] (#7890) 2022-10-19 15:25:15 +13:00
Dakota Walsh
a308cf4ae4 fix(kubernetes): create proxied kubeclient EE-4326 (#7851) 2022-10-18 11:05:42 +13:00
itsconquest
9c04c253e7 fix(UAC): provide required UI context [EE-4415] (#7853) 2022-10-18 09:45:43 +13:00
Prabhat Khera
446e1822bf fix reloading page when ing class disallowed (#7831) 2022-10-17 10:44:22 +13:00
Ali
4a27aa12bc fix(ing): nodeport validate and show errors (#7802) 2022-10-12 10:06:33 +13:00
andres-portainer
a9973c8d53 fix(build): add -trimpath EE-4406 (#7835) 2022-10-11 13:01:04 -03:00
andres-portainer
677d61b855 fix(logging): convert missing cases to Zerolog EE-4400 (#7816) 2022-10-11 12:59:07 -03:00
Oscar Zhou
b0938875dc fix(gitops): update the git ref cache key from url to url and pat (#7840) 2022-10-11 18:31:28 +13:00
itsconquest
1e1cb3784c fix(notifications): cleanup notifications code [EE-4274] (#7789)
* fix(notifications): cleanup notifications code [EE-4274]

* break long words
2022-10-11 14:05:50 +13:00
Ali
f1e7417e33 fix(ingress): update ingress tls after deletion EE-4387 (#7805)
* fix(ing): update tls value EE-4387
2022-10-10 09:32:37 +13:00
Ali
c45d41f55f fix(clustersetup): dont show modal when loading (#7811) 2022-10-08 17:48:39 +13:00
Ali
e02238365a fix(application): edit cluster ip services EE-4328 (#7774) 2022-10-07 16:55:25 +13:00
congs
6e1e9e9341 fix(UI): EE-4381 environment ID is shown instead of its name when deleting an environment (#7809) 2022-10-07 16:36:26 +13:00
congs
eb0cfdd2c1 fix(wizard): EE-4350 Environment creating script should only showed for relevant type of environment (#7787) 2022-10-07 15:43:12 +13:00
congs
44f74a7441 fix(help): EE-4335 context sensitive help improvement (#7755) 2022-10-07 14:25:34 +13:00
matias-portainer
4ee064eeee fix(edge): fix docker proxy EE-4380 (#7800) 2022-10-06 11:12:44 -03:00
Ali
da0661a1bd fix(ingress): ingress indicate missing services EE-4358 (#7795) 2022-10-06 15:25:09 +13:00
Dakota Walsh
19c2bc12de fix(ingress-controllers): rework namespace allow and disallow EE-4322 (#7743)
* fix(ingress-controllers): rework namespace allow and disallow

* add check for ingressAvailabilityPerNamespace

Co-authored-by: Prabhat Khera <prabhat.khera@portainer.io>
2022-10-05 15:50:48 +13:00
Prabhat Khera
bc6a43d88b bug(ingress): fix ingress class disallowed to not found issue EE-4311 (#7749) 2022-10-05 15:17:46 +13:00
Rex Wang
6d6632d4e2 fix(docker): fix text info format [EE-2681] (#7762)
* EE-2681 fix(docker): fix text info format

* EE-2681 fix(docker): revert message changes

* EE-2681 fix(docker) add missing space
2022-10-04 09:12:16 +08:00
Ali
a32b004fb3 fix(cluster): fix cluster setup no ingress release EE-4352 (#7777)
* fix(cluster) update cluster wo controllers EE-4352

* fix(ing): stop errors in ns EE-4352
2022-10-04 12:14:02 +13:00
Ali
ba441da519 fix(deploy): update option text EE-4362 (#7782) 2022-10-04 10:20:22 +13:00
Ali
07f8abe2f3 fix(customtemplate) fix custom var payload EE-4340 (#7753) 2022-10-03 09:49:34 +13:00
Xuing
071962de2d fix(readme) update deploy portainer url (#7760)
(cherry picked from commit a0fa64781a)
2022-09-30 14:50:03 +13:00
Ali
04fd2a2b44 fix(clustersetup): set a default access mode (#7746) 2022-09-29 10:26:22 +13:00
Ali
70d89e9a24 fix(secrets): fix edit, refactor form type (#7734) 2022-09-29 09:57:29 +13:00
LP B
e5f8466fb9 fix(app/environments): retain previously selected environments [EE-3233] (#7358) 2022-09-26 19:00:10 -03:00
Yi Chen
c3110a85b2 * replace npm mirror with yarnpkg (#7730) 2022-09-27 10:08:47 +13:00
Dakota Walsh
89eda13eb3 feat(ingress): autodetect ingress controllers EE-673 (#7712) 2022-09-27 08:43:24 +13:00
Hao
c96551e410 feat(stack): rebuild image for compose stack from git [EE-2681] (#7707)
* feat(stack): rebuild image for compose stack from git [EE-2681]

* feat(stack): rebuild image for compose stack from git [EE-2681]

* --no-edit

* UI
2022-09-26 14:22:38 +08:00
Rex Wang
9f7d5ac842 fix(docker): stack's env vars support empty value EE-1528 (#7592)
* EE-1528 fix(docker): stack's env vars support empty value

* EE-1528 fix(docker): handle no-value env as empty env
2022-09-24 20:05:20 +08:00
itsconquest
648c1db437 feat(notifications): track toast notifications [EE-4132] (#7711)
* feat(notifications): track toast notifications [EE-4132]

* suggested refactoring

* fix failing test

* remove duplicate styles

* applying spacing to context icon
2022-09-23 17:17:44 +12:00
Ali
4e20d70a99 feat(secrets): allow creating secrets beyond opaque [EE-2625] (#7709) 2022-09-23 16:35:47 +12:00
fhanportainer
3b2f0ff9eb fix(access-token): fixed create access token view. (#7716) 2022-09-23 16:29:25 +12:00
Prabhat Khera
fcb76f570e feat(ingress): remove ingresses from add and edit application EE-4206 (#7677) 2022-09-23 16:11:35 +12:00
itsconquest
c384d834f5 fix(build): restore aliases for uppercase imports [EE-4312] (#7723) 2022-09-23 15:55:05 +12:00
Dmitry Salakhov
45e2ed3d86 fix: miscofigured logging statements (#7721) 2022-09-23 13:15:26 +12:00
matias-portainer
6e0f83b99e feat(snapshots): separate snapshots from endpoint DB struct EE-4099 (#7614) 2022-09-22 17:05:10 -03:00
Prabhat Khera
4fe2a7c750 fix ingress screen loading (#7715) 2022-09-22 16:12:19 +12:00
congs
f8b8d549fd feat(help): EE-2724 Context sensitive help (#7694) 2022-09-22 13:39:36 +12:00
LP B
1b0db4971f feat(app/logs): format Zerolog in logs viewer [EE-4226] (#7685)
* feat(app/logs): format Zerolog in logs viewer

* fix(app/logs): trim caller to only last 2 segments
2022-09-22 00:34:58 +02:00
LP B
6063f368ea fix(api/snapshot): convert error message only on matching env types (#7661) 2022-09-22 00:34:14 +02:00
Chao Geng
8ef584e41c feat(docker): new version message in BE side menu [EE-4079] (#7680)
* export GetLatestVersion and HasNewerVersion
2022-09-21 17:22:39 +08:00
Chaim Lev-Ari
ceaee4e175 refactor(ui): replace ng selectors with react-select [EE-3608] (#7203)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2022-09-21 10:10:58 +03:00
Chaim Lev-Ari
1e21961e6a refactor(app): move settings components to react [EE-3442] (#7625) 2022-09-21 09:14:29 +03:00
Oscar Zhou
5777c18297 feat(gitops): support to list git repository refs and file tree [EE-2673] (#7100) 2022-09-21 17:47:02 +12:00
Prabhat Khera
ef1d648c07 feat(ingress): ingresses datatable with add/edit ingresses EE-2615 (#7672) 2022-09-21 16:49:42 +12:00
Dmitry Salakhov
393d1fc91d fix: braking changes in compose (#7708) [EE-4258] 2022-09-21 15:59:40 +12:00
andres-portainer
f9fe440401 feat(logging): trim paths from the build EE-4186 (#7710) 2022-09-20 18:48:07 -03:00
Chaim Lev-Ari
fad376b415 refactor(ui): remove global providers [EE-4128] (#7578) 2022-09-20 21:14:24 +03:00
Chao Geng
d3f094cb18 fix(image): better URL info text and a link to documentation to build image [EE-2409] (#7641)
* URL info text and a link to documentation to build image
2022-09-20 13:42:31 +08:00
Matt Hook
1950c4ca2b Sanitze kube labels (#7658) 2022-09-20 16:19:54 +12:00
Chao Geng
5232427a5b updated k8s stack deployment specification in Swagger (#7619) 2022-09-20 06:59:14 +08:00
andres-portainer
0fac1f85f7 feat(logging): redirect the standard logger to Zerolog EE-4186 (#7702) 2022-09-19 15:39:43 -03:00
Hao
70ce4e70d9 fix(registry): fix anonymous dockerhub name to make it same with BE [EE-4208] (#7690) 2022-09-19 16:52:15 +08:00
congs
47f2490059 fix(wizard) EE-2053 Add Docker Standalone option to agent install instructions (#7589) 2022-09-19 13:44:52 +12:00
Chaim Lev-Ari
4d123895ea feat(edge/update): select endpoints to update [EE-4043] (#7602) 2022-09-18 14:42:18 +03:00
andres-portainer
36e7981ab7 feat(logging): replace all the loggers with zerolog EE-4186 (#7663) 2022-09-16 13:18:44 -03:00
Oscar Zhou
53025178ef fix(access): support to list users or teams with specified endpoint [EE-1704] (#7610) 2022-09-16 14:45:14 +12:00
Rex Wang
f71fe87ba7 fix(docker): ui link style [EE-4184] (#7655)
* EE-4184 fix(docker): ui link style
2022-09-15 17:33:49 +08:00
congs
6078234d07 fix(stack): EE-4213 Allow latest image to be pulled for stacks: backport backend logic (#7669) 2022-09-15 16:57:26 +12:00
Oscar Zhou
fa162cafc1 feat(gitops): support to store git credentials [EE-2683] (#7066) 2022-09-15 16:32:05 +12:00
andres-portainer
9ef5636718 chore(handlers): replace structs by functions for HTTP errors EE-4227 (#7664) 2022-09-14 20:42:39 -03:00
Matt Hook
7accdf704c fix(kube): change warning text colour to match figma styling [EE-3045] (#7582)
* update warning text colour, icon and alignment to match figma
2022-09-15 11:09:19 +12:00
Chao Geng
d570aee554 feat(image): upload local files for building image EE-3021 (#7507)
* support to make multiple files in archive buffer

* upload files by multipart
2022-09-14 14:47:24 +08:00
Chao Geng
a7d458f0bd chore(tests): use t.TempDir to create temporary test directory [EE-3700] (#7612)
* create temporary test directory with t.TempDir
2022-09-14 13:59:47 +08:00
congs
1a9d793f2f fix(stack): EE-4213 Allow latest image to be pulled for stacks (#7653) 2022-09-14 10:17:32 +12:00
fhanportainer
0242c8e4ef fix(dropdown): fixed dropdown menu background color in dark mode. [EE-4026] (#7591)
* fix(dropdown): fixed dropdown menu background color in dark mode. [EE-4026]

* fix(dropdown): fixed table setting background color in dark mode.

* fix(dropdown): updated --bg-dropdown-menu-color in dark theme.

* fix(dropdown): fixed dropdown border radius issue

* fix(dropdown): fixed dropdown option text color in dark mode
2022-09-14 10:16:02 +12:00
Chaim Lev-Ari
6c4c958bf0 feat(edge/update): remote update structure [EE-4040] (#7553) 2022-09-13 16:56:38 +03:00
itsconquest
dd1662c8b8 fix(extension): change ports to reduce conflicts [EE-3211] (#7596) 2022-09-13 11:03:37 +12:00
LP B
fdfebcf731 fix(style): autofilled inputs use theme colors [EE-3828] (#7576) 2022-09-12 16:29:15 +02:00
itsconquest
9ce3e7d20d fix(theme): tabs and codeeditor darkmode correction [EE-4188] (#7643)
* fix(theme): tabs and codeeditor darkmode correction [EE-4188]

* correct codemirror background

* fix typo
2022-09-12 17:07:03 +12:00
congs
bf8b9463d3 fix(security): EE-3202 Portainer CE and EE JS Dependencies (#7561) 2022-09-12 13:32:58 +12:00
Oscar Zhou
9375e577b0 feat(setting): display custom banner option as the limited feature for be (#7590) 2022-09-09 13:29:30 +12:00
itsconquest
d95a67a567 fix(theme): env sidebar darkmode color [EE-4188] (#7638)
* fix(theme): env sidebar darkmode color [EE-4188]

* style usericon

* further dark mode changes
2022-09-09 12:47:06 +12:00
Dmitry Salakhov
160e210ffe feat: update compose and helm versions (#7536) [EE-3205] 2022-09-09 11:26:56 +12:00
itsconquest
c9eaad6237 fix(auth): prevent trim on password [EE-4197] (#7633) 2022-09-08 13:50:21 +12:00
itsconquest
2edff939ef fix(theme): update dark mode colors [EE-4188] (#7623)
* fix(theme): update dark mode colors [EE-4188]

* fix sidebar hover/selected
2022-09-08 13:49:09 +12:00
congs
13338c46bb fix(wizard): EE-3728 Metadata is not working with Nomad (#7615) 2022-09-08 13:11:57 +12:00
LP B
ea05814af4 fix(images/build): enforce file content length only when using the editor (#7630) 2022-09-08 02:32:36 +02:00
Dmitry Salakhov
0fe2ddf535 fix: don't url-escape socket paths (#7627) 2022-09-08 11:44:50 +12:00
Rex Wang
9af9395b73 fix(docker): prevent misconfigured stack from saving EE-3235 (#7585)
* EE-3235 fix(docker): add checker to editor

* support rollback to update stack file

Co-authored-by: chaogeng77977 <chao.geng@portainer.io>
2022-09-07 16:50:59 +08:00
Chaim Lev-Ari
d9cc7eda51 refactor(app): move access-control components [EE-3441] (#7559) 2022-09-07 07:25:00 +03:00
fhanportainer
77c3f9131b fix(environment): update page title when no environment selected. (#7606)
* fix(environment): update page title when no environment selected.

* fix(environment): update page title when clearing environment from side bar.

* fix(environment): update page title when clearing environment from a non-environment page.
2022-09-07 11:08:45 +12:00
Dakota Walsh
2b2580fb61 fix(kubernetes): gke node stats (#7455) 2022-09-07 10:39:00 +12:00
congs
f870619fb6 fix(git): EE-3727 nomad extension not available (#7595) 2022-09-06 10:54:21 +12:00
LP B
602e42739e feat(stacks): remove the ability to delete external swarm stacks [EE-2611] (#7560) 2022-09-05 15:00:49 +02:00
Rex Wang
326a8abdc7 EE-4021 fix(docker): rename deployed container (#7601) 2022-09-05 17:39:08 +08:00
Rex Wang
c0f3d0193d EE-4125 fix(docker): fix creating container UI style (#7607) 2022-09-05 07:08:38 +08:00
Chaim Lev-Ari
f9427c8fb2 refactor(teams): migrate teams to react [EE-2273] (#6691)
closes [EE-2273]
2022-09-02 18:30:34 +03:00
huib-portainer
9b02f575ef chore(readme): update readme to remove the outdated demo 2022-09-02 13:53:47 +12:00
itsconquest
5b4f6098d8 fix(boxselector): fix darkmode BE teaser style [EE-4145] (#7598)
* fix(boxselector): fix darkmode BE teaser style [EE-4145]

* make opacity same when selected

* add missing link to teaser

* style unchecked boxes + light mode

* revert colors for ligh theme
2022-09-02 12:42:48 +12:00
Oscar Zhou
ccaf2bedb7 fix(stack/compose): remove the orphan containers if stack deployment is failed (#7599) 2022-09-02 08:11:02 +12:00
Rex Wang
88757d2617 fix(docker): style fixes [EE-4024] (#7569)
* EE-4042 update docker screens trash icon

* EE-4024 fix(docker): change styles
2022-09-01 19:02:21 +08:00
Matt Hook
d79586cf6a chore(readme): update readme to display latest version (#7604)
* use badge to display latest version 
* use markdown syntax
2022-09-01 14:04:59 +12:00
Rex Wang
a9b1a9c194 fix(docker): don't trimming when creating secret [EE-3265] (#7577)
* EE-3265 fix(docker): stop trimming when creating secret

* EE-3265 fix(docker): stop triming when creating secret in k8s
2022-08-31 23:19:14 +08:00
fhanportainer
eb5036b96f fix(docker): removed docker.sock code in docker API [EE-3612] (#7586) 2022-08-31 20:32:01 +12:00
LP B
2f0dbf2ae1 fix(container/edit): fallback value when retrieving GPU config without snapshot available [EE-4110] (#7570) 2022-08-30 14:52:24 +02:00
itsconquest
c79be58700 fix(sidebar): rework the update notification [EE-4119] (#7575) 2022-08-30 10:00:12 +12:00
Oscar Zhou
d24e5ff71e feat(docker/container): support --shm-size configuration [EE-550] (#7547) 2022-08-30 09:22:27 +12:00
Chaim Lev-Ari
6536d36c24 feat(ui): hide user menu on docker extension [EE-4115] (#7563) 2022-08-29 05:07:07 +03:00
wheresolivia
6174940ac2 add data-cy attributes for docker image tag selectors (#7581) 2022-08-29 13:46:06 +12:00
fhanportainer
4c98fcd7db feat(analytis): EnableTelemetry defaults to false (#7539) 2022-08-29 11:09:47 +12:00
congs
ad8054ac1f fix(stack): EE-3908 broken modal when updating/redeploying stacks: turn off toggle (#7573) 2022-08-26 17:54:10 +12:00
Matt Hook
a54c54ef24 fix(swarm): fixed issue parsing url with no scheme [EE-4017] (#7502) 2022-08-26 11:55:55 +12:00
itsconquest
27095ede22 fix(stacks): orphaned stacks readonly [EE-4085] (#7552)
* fix(stacks): orphaned stacks readonly [EE-4085]

* correctly handle stack type in controller
2022-08-25 10:27:12 +12:00
congs
e2789ab354 fix(container): EE-3995 gpus console error under stack list page (#7530) 2022-08-25 10:27:02 +12:00
Matt Hook
d4f4bb532f fix(web-editor): add search hint text [EE-3967] (#7496) 2022-08-25 10:11:25 +12:00
Zhang Hao
c6ab5d5717 fix(image): Add hide default registry teaser for CE version [EE-4038] (#7533)
* fix(image): Add hide default registry teaser for CE version [EE-4038].

* fix(image): Hide advanced mode only if there is no docker hub registries [EE-3734]

* sync with EE
2022-08-24 19:33:48 +08:00
Chaim Lev-Ari
234627f278 fix(ui/buttons): set hyperlink style [EE-4007] (#7524) 2022-08-24 07:40:50 +03:00
itsconquest
87214d48be fix(wizard): highcontrast style for BE only options (#7544) 2022-08-24 14:48:05 +12:00
itsconquest
a2a35a1851 fix(azure): correctly sort container ports [EE-4076] (#7550) 2022-08-24 12:43:04 +12:00
Chaim Lev-Ari
11f0574ad3 fix(stack): hide containers for swarm stack [EE-3969] (#7504) 2022-08-23 09:47:09 +03:00
Chaim Lev-Ari
9fbc6177a6 fix(stack): hide containers for swarm stack [EE-3969] (#7503) 2022-08-23 09:46:55 +03:00
congs
b91e06a60a fix(stack): EE-3908 broken modal when updating/redeploying stacks (#7497) 2022-08-23 14:22:18 +12:00
fhanportainer
ad3f4ff711 fix(toggle): fixed disabled toggle color in dark and high contrast modes. (#7518)
* fix(toggle): fixed disabled toggle color in dark and high contrast modes.

* fix(switch): fixed switch color in dark and high contrast modes.

* fix(switch): fixed switch in LDAP secion.

* fix(switch): corrected the blue color of Switch in dark and high contrast themes.
2022-08-23 12:11:11 +12:00
Prabhat Khera
7edcfd6eab fix minor ui issues (#7510) 2022-08-23 08:55:41 +12:00
Prabhat Khera
735b2063ea fix(ui): minor ui issues EE-4004 (#7512) 2022-08-23 08:54:40 +12:00
matias-portainer
bce4d02dd2 fix(edge): save edge checkin interval during endpoint creation (#7541) 2022-08-22 12:08:53 -03:00
fhanportainer
e84126ec13 feat(label): uses --ui-white for control-label css class in Dark and High contrast themes (#7505)
* feat(label): uses `--ui-white` for control-label css class in Dark and High contrast themes.

* feat(label): uses `apply` in control-label css class.
2022-08-23 03:08:29 +12:00
matias-portainer
3a324acb0e fix(edge): fix edge URL placeholder EE-2598 (#7459) 2022-08-22 10:13:01 -03:00
Rex Wang
c6f7427283 Fix(UI) fix color of file upload button in dark mode EE-4009 (#7535)
* fix snapshot url parsing issue for ip addresses (#7477)

* fix(ui/header): change font sizes [EE-3966] (#7484)

* fix(k8s/apps): show horizontal scrollbar [EE-3941] (#7476)

* fix(kubeconfig): update button and modal styles (#7480)

EE-3947

* fix(containers): make table wider [EE-3944] (#7486)

* Fix(UI) UI fixes on docker container screens (release/2.15) EE-3915 (#7499)

* EE-3915 ui fixes on docker container screens

* Update createcontainer.html

Update label

* EE-3916 fix container link under stack detail page (#7508)

* EE-4009 fix color of file-upload button in dark mode

Co-authored-by: Matt Hook <hookenz@gmail.com>
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
2022-08-22 20:25:26 +08:00
Chaim Lev-Ari
ace01eac9d fix(ui): box-selector fixes [EE-3949] (#7489) 2022-08-22 11:55:48 +03:00
Ali
8d304b78cb fix(kubeshell): add back data-cy EE-4054 (#7538) 2022-08-22 16:52:24 +12:00
fhanportainer
c17baa36ef fix(app-template): fixed the app template list not scroll to top issue (#7519)
* fix(app-template): fixed the app template list not scroll to top issue

* fix(templates): added id prop to PageHeader component.
2022-08-20 00:31:17 +12:00
fhanportainer
8cbff097e4 feat(docker): fixed info icon in docker feature config section. (#7492) 2022-08-19 12:55:29 +12:00
Matt Hook
294738cb0d bump version to 2.16 (#7475) 2022-08-19 10:34:47 +12:00
Rex Wang
69bc815acd EE-3998 bug fix (#7522) 2022-08-18 18:56:37 +08:00
Prabhat Khera
fb62edefbc fix background (#7515) 2022-08-18 22:39:54 +12:00
fhanportainer
5e35ff8b8a feat(stack): fixed stack web editor scroll bar issue. (#7494) 2022-08-18 18:00:33 +12:00
Rex Wang
20053b1f07 EE-3916 fix container link under stack detail page (#7509) 2022-08-17 23:48:21 +08:00
Rex Wang
cc6c5d45b7 Fix(UI) UI fixes on docker container screens EE-3915 (#7500)
* EE-3915 ui fixes on docker container pages

* Update createcontainer.html

Update label
2022-08-17 23:37:35 +08:00
Chaim Lev-Ari
f480e0ccf6 fix(containers): make table wider [EE-3944] (#7487) 2022-08-17 12:49:29 +03:00
Ali
d85149e328 fix(kubeconfig): update button and modal styles (#7481)
EE-3947
2022-08-17 20:01:04 +12:00
Chaim Lev-Ari
cee241e77c fix(k8s/apps): show horizontal scrollbar [EE-3941] (#7472) 2022-08-16 20:59:01 +03:00
Chaim Lev-Ari
8ec9515225 fix(activity): fix angularjs error [EE-3968] (#7482) 2022-08-16 18:08:48 +03:00
Chaim Lev-Ari
d4ffaaef2f fix(activity): fix angularjs error [EE-3968] (#7483) 2022-08-16 18:08:21 +03:00
Chaim Lev-Ari
eda8347091 fix(ui/header): change font sizes [EE-3966] (#7485) 2022-08-16 18:08:08 +03:00
matias-portainer
4c23513a41 fix(home): remove edge devices from homepage list EE-3919 (#7471) 2022-08-16 09:57:55 -03:00
Matt Hook
81d1f35bdc fix snapshot url parsing issue for ip addresses (#7478) 2022-08-16 10:36:12 +12:00
Ali
36c93c7f57 fix(ui): kubernetes-consistent-styling EE-3820 (#7425) 2022-08-13 00:22:45 +06:00
Rex Wang
b67f404d8d EE-3905 changes for item 1,2,3,4,9,10,12,13,14 (#7467) 2022-08-12 12:47:44 +08:00
Chaim Lev-Ari
95fb5a4baa fix(ui): fix ui bugs [EE-3847] (#7453) 2022-08-12 15:47:56 +12:00
matias-portainer
dd372637cb feat(ui): renovate the edge devices waiting room (#7456) 2022-08-12 15:01:31 +12:00
Chaim Lev-Ari
c1a4856e9d feat(ui/datatables): add styles for nested tables [EE-3687] (#7440)
* feat(ui/datatables): add styles for nested tables
2022-08-12 14:56:48 +12:00
Chaim Lev-Ari
92b7e64689 feat(ui/sidebar): support custom logos [EE-3753] (#7436)
* feat(ui/sidebar): show right logos
2022-08-12 13:27:30 +12:00
matias-portainer
a750259a2c fix(edge): generate new EdgeID only if not present (#7454) 2022-08-11 22:23:13 -03:00
matias-portainer
87accfce5d fix(edge): parse agent platform on every polling request to avoid endpoint misconfiguration (#7452) 2022-08-11 22:21:56 -03:00
Chaim Lev-Ari
29f0daa7ea fix(edge/stacks): show correct status for env [EE-3374] (#7466) 2022-08-11 22:20:36 -03:00
Richard Wei
a247db7e93 feat(ui): added teaser styling for CE EE-3780 (#7323)
* added teaser styling for CE
2022-08-12 12:03:30 +12:00
itsconquest
1fbaf5fcbf fix(style): correct common pages [EE-3886] (#7449)
* fix(css): correct common pages [EE-3886]
2022-08-12 11:58:31 +12:00
Chaim Lev-Ari
c981e6ff7b fix(home): clear all filters [EE-3912] (#7465) 2022-08-12 02:00:33 +03:00
Richard Wei
ee1ee633d7 feat(ui): portainer wizard ui change for ce EE-3576 (#7405)
* ui change for wizard
2022-08-12 08:43:01 +12:00
Ali
a7ab0a5662 feat(ui): box-selector-style-updates EE-3698 (#7382) 2022-08-11 14:13:11 +06:00
Chaim Lev-Ari
bed4257194 refactor(containers): migrate view to react [EE-2212] (#6577)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2022-08-11 07:33:29 +03:00
Chaim Lev-Ari
5ee570e075 feat(home): filter by connection type and agent version [EE-3373] (#7085) 2022-08-11 07:32:12 +03:00
Rex Wang
9666c21b8a EE-3860 fix typo (#7447) 2022-08-11 07:40:26 +08:00
Oscar Zhou
5cf789a8e4 fix(yarn): update yarn lock file to fix nightly code scan failure (#7460) 2022-08-11 10:28:58 +12:00
Matt Hook
6a4a353b92 feat(environment): update wording when editing agent environment [EE-3081] (#7445)
* change wording when editing agent environment
2022-08-11 09:27:35 +12:00
Prabhat Khera
02355acfa8 fix(ui): namespace name sort EE-3863 (#7442) 2022-08-11 09:25:29 +12:00
congs
04eb718f88 fix(gpu) EE-3191 fix gpu bugs (#7451) 2022-08-11 09:05:27 +12:00
congs
36888b5ad4 feat(ui): EE-3567 css portainer settings auth (#7423) 2022-08-10 17:49:43 +12:00
Ali
7bd971f838 fix(toast): update styles and custom button (#7450)
EE-3829
2022-08-10 17:07:35 +12:00
Chaim Lev-Ari
c3ce4d8b53 feat(sidebar): add dark theme colors [EE-3666] (#7414) 2022-08-10 07:12:20 +03:00
Ali
fb3a31a4fd feat(login): allow-show-hide-password-in-login-screen EE-3885 (#7433)
* feat(login): allow show/hide password EE-3885
2022-08-10 16:07:24 +12:00
Rex Wang
b6852b5e30 fix(UI) registry page improvement EE-2705 (#7424)
* EE-2705 bug fix

* EE-2705 hide auth switch for gitlab
2022-08-10 09:07:20 +08:00
Richard Wei
34e2178752 feat(ui): portainer registry boxselector icon ce EE-3848 (#7419)
* add icon to registry boxselector
2022-08-10 12:21:17 +12:00
fhanportainer
83a17de1c0 feat(roles): fixed search box in the Roles page. (#7448)
* feat(roles): fixed search box in the Roles page.

* feat(roles): fixed icon position
2022-08-09 19:22:10 +12:00
Oscar Zhou
e5b27d7a57 fix(ldap/tls): allow to upload tls ca certificate [EE-3654] (#7340) 2022-08-09 17:19:32 +12:00
Richard Wei
fb14a85483 fix search box for group (#7446) 2022-08-09 12:43:37 +12:00
Zhang Hao
8d4cb5e16b fix(container): some style bug on container create page [EE-3744] (#7415)
* fix(container): some style bug on container create page [EE-3744]
2022-08-09 07:50:18 +08:00
Richard Wei
ad8b8399c4 fix(ui): remove right label for switch toggle EE-3786 (#7349)
* remove right label for switch toggle
2022-08-08 16:43:31 +12:00
Dakota Walsh
8ff2fa66b6 fix(kube): update kubectl agent install instructions (#7421) 2022-08-08 14:06:10 +12:00
Zhang Hao
539948b5a6 fix(container): fixed add value and remove value for env [EE-3839] (#7429)
* fix(style): UI task issues [EE-3839]
2022-08-05 15:45:21 +08:00
Dmitry Salakhov
bfe1cace77 fix: correctly flagged pull latest image feature as limited (#7428) 2022-08-05 16:05:49 +12:00
Dakota Walsh
7b806cf586 fix(cache): trigger page reload on logout (#7407)
When portainer is restarted the user's session is invalidated and as
soon as they start clicking around they will be logged out. This PR does
two additional things when this happens.

1) We trigger a browser page reload which will force the client the grab
   the latest version of our js, css, etc. Previously if a user updated
   portainer, but never clicked the browser's refresh button they would
   never see new css changes.

2) We also set "cache-control no-cache" on the index.html header. Since
   portainer is an SPA and the the index.html is very small it makes
   sense to avoid letting the browser cache it so that the user is
   always given the latest version when the above reload is triggered.
2022-08-05 15:38:45 +12:00
Rex Wang
69a824c25b Fix(UI) Update UI of docker dashboard EE-3845 (#7422)
* EE-3846 fix alignment of left-hand side of fields
2022-08-05 10:17:31 +08:00
itsconquest
8d733ccc8c style(init): style init views [EE-3556] (#7384)
* style(init): style init views [EE-3556]

* update icons, alignment & allow clicking feature indicator link

* update cursor style on hover
2022-08-05 11:48:08 +12:00
Rex Wang
2574f223b4 fix(UI) check image name when build image EE-3010 (#7409)
* EE-3010 check image name when build image
2022-08-05 07:04:26 +08:00
fhanportainer
f2d93654f5 feat(roles): updated roles view css UI (#7389)
* feat(roles): updated roles view css UI

* feat(roles): updated icons
2022-08-05 10:26:33 +12:00
fhanportainer
5e74b90780 feat(teams): updated teams edit css UI (#7403)
* feat(teams): updated teams edit css UI

* feat(team): removed inline style.
2022-08-05 10:25:29 +12:00
fhanportainer
78ce176268 feat(teams): teams page css UI update. (#7402)
* feat(teams): teams page css UI update.

* feat(teams): added `required` attr to team name field

* feat(teams): fixed remove and search bar position

* feat(teams): fixed CreateTeamForm unit test
2022-08-05 10:24:19 +12:00
Matt Hook
dfb398d091 import the search feature (#7426) 2022-08-05 10:21:26 +12:00
Matt Hook
4e9b3a8940 fix(endpoint handler): fix endpoint address(url) parsing EE-3081] (#7408)
fix address validation when creating agent endpoint
2022-08-05 09:30:54 +12:00
Zhang Hao
0141e55936 fix(style): UI task issues [EE-3839] (#7406) 2022-08-04 23:41:12 +08:00
Rex Wang
46fba176f0 EE-3846 fix alignment of left-hand side of fields (#7413) 2022-08-04 22:05:27 +08:00
andres-portainer
441e265c32 feat(ui): renovate the docker images edit page EE-3505 (#7375) 2022-08-04 10:10:13 -03:00
Dakota Walsh
d28030abea feat(ui): namespace details UI improvements EE-3480 (#7335) 2022-08-04 14:45:44 +12:00
Dakota Walsh
aa0f1221de feat(ui): namespace access EE-3478 (#7398) 2022-08-04 13:29:55 +12:00
Richard Wei
305a949692 feat(ui): ui change for auth and activity logs EE-3798 (#7364)
* added style to authentication and activity logs
2022-08-04 11:48:05 +12:00
Richard Wei
31d3fd730c fix(ui): fix users teams missing from menu for teamlead EE-3761 (#7381)
* fix users & teams missing from menu for teamlead
2022-08-04 09:23:38 +12:00
congs
a46002502f feat(ui): EE-3719 css portainer environments access (#7359) 2022-08-04 09:05:33 +12:00
wheresolivia
56fcc91e30 fix(data-cy): rename the duplicated data-cy attribute in kube data table pages [EE-3749] (#7416)
rename the duplicated data-cy attributes in kube stack and port data table page
2022-08-04 08:28:37 +12:00
wheresolivia
8a8058e4eb fix(data-cy): rename the duplicated data-cy attribute in kube stack data table page [EE-3749] (#7411)
rename the duplicated data-cy attribute in kube stack and port data table page
2022-08-04 06:27:28 +12:00
andres-portainer
20a66fb10f fix(endpoints): remove global map to avoid panic writes EE-3838 (#7404) 2022-08-03 12:18:33 -03:00
Ali
628f822025 fix(stacks): enforce stack permissions for non admin users EE-3683 (#7399)
* fix(stacks): hide stacks in sidebar EE-3683

* fix(stacks): for unauth, take the user to the dashboard

* fix(stacks): block the user from stack details EE-3683

* fix(stacks): disable stack managment for non admins
2022-08-03 22:19:27 +12:00
Rex Wang
d8db8718bd EE-3831 Replace sort icon and search icon in all docker pages (#7400) 2022-08-03 17:43:29 +08:00
congs
5b40c79ea3 feat(ui): EE-3801 css portainer groups new (#7362) 2022-08-03 17:16:49 +12:00
Ali
ae9025c1fb feat(ui): kubernetes-volumes-list EE-3484 (#7290)
* feat(ui) volumes datatable styling EE-3484

* feat(ui): storage datatable styling EE-3484
2022-08-03 15:53:59 +12:00
fhanportainer
0014e39b61 feat(user): updated user edit view css UI (#7373) 2022-08-03 13:24:42 +12:00
Zhang Hao
5d1ea8ceb2 feat(secret&icon): secret creation page and some other icons [EE-3510] (#7357) 2022-08-03 08:56:29 +08:00
Matt Hook
079478f191 restyle (#7350) 2022-08-03 12:05:16 +12:00
Richard Wei
65c050dc87 feat(ui): ui change for edge compute settings EE-3800 (#7365)
* added style to edge compute under settings
2022-08-03 11:15:00 +12:00
congs
21fbd37bfb feat(ui): EE-3718 css portainer environments edit (#7318) 2022-08-03 10:19:28 +12:00
matias-portainer
b28f635fb2 feat(ui): renovate the edge jobs edit page EE-3531 (#7192) 2022-08-02 10:28:27 -03:00
matias-portainer
0580d3833a feat(ui): renovate the edge jobs create page EE-3530 (#7188) 2022-08-02 10:26:58 -03:00
Prabhat Khera
bff9bb7800 feature(ui): registry access screen EE-3770 (#7332) 2022-08-02 15:32:22 +12:00
Prabhat Khera
fb3d333453 fix(registries): Cannot read properties of null error on change of namespace EE-3747 (#7363) 2022-08-02 14:39:53 +12:00
congs
2c25e1d48e feat(ui): EE-3571 css portainer tags (#7383) 2022-08-02 14:22:20 +12:00
Ali
5469392ec7 feat(ui): config-details-styling EE-3472 (#7367)
* feat(ui): config details EE-3472
2022-08-02 14:21:14 +12:00
Prabhat Khera
e1c7079c81 feat(ui): ui improvements create template EE-3628 (#7352) 2022-08-02 14:10:39 +12:00
Richard Wei
75c1b485ab feat(ui): css tidy up for ui change EE-3795 (#7354)
* css tidy up for ui change
2022-08-02 12:17:22 +12:00
congs
03590d46e6 feat(ui): EE-3767 css portainer groups (#7360) 2022-08-02 11:19:57 +12:00
andres-portainer
9dc6aa81cb feat(ui): renovate the edge group creation page EE-3527 (#7191) 2022-08-01 18:24:05 -03:00
andres-portainer
d0b88d7e2f feat(ui): renovate the edge stacks edition page EE-3534 (#7213) 2022-08-01 17:48:41 -03:00
andres-portainer
5343b965aa feat(ui): renovate the docker images import page EE-3504 (#7374) 2022-08-01 17:19:07 -03:00
andres-portainer
104c82c54e feat(ui): renovate the edge groups list page EE-3529 (#7186) 2022-08-01 17:11:09 -03:00
andres-portainer
c0569a0752 feat(ui): renovate the Docker volume edit page EE-3515 (#7379) 2022-08-01 17:09:11 -03:00
matias-portainer
ad86b6b11f feat(ui): renovate the edge stacks creation page EE-3533 (#7319) 2022-08-01 15:33:18 -03:00
andres-portainer
ff32e87b97 feat(ui): renovate the Docker volume creation page EE-3514 (#7380) 2022-08-01 14:46:09 -03:00
andres-portainer
1e78234f04 feat(ui): renovate the Docker volume list page EE-3513 (#7377) 2022-08-01 14:44:44 -03:00
Zhang Hao
d0a9c046b3 refactor(docker/stack): stack creation page [EE-3486] (#7316)
* reactor(docker/stack): stack creation page [EE-3486]

* feat(stack): some missing component on stack create page and edit page [EE-3486]
2022-08-01 23:07:41 +08:00
Zhang Hao
c54bb255ba feat(container): container detail page as well as some icon changes [EE-3493] (#7361) 2022-08-01 23:06:39 +08:00
matias-portainer
8843b7b0e8 feat(ui): renovate the docker images build page EE-3503 (#7387) 2022-08-01 10:51:20 -03:00
Rex Wang
a95d734c34 EE-3487 update ui of docker/configs (#7370) 2022-08-01 20:31:56 +08:00
Rex Wang
8262487401 fix(UI) update ui of swarm/node/item EE-3518 (#7392)
* EE-3502 update page docker/host/browse and docker/volume/browse

* EE-3518 update ui of swarm/node/item
2022-08-01 16:14:43 +08:00
Ali
57e53d1a21 feat(ui): ui-improvements-helm EE-3476 (#7344)
* feat(ui): helm views ui update EE-3476
2022-08-01 19:13:58 +12:00
Rex Wang
e28a1491d4 EE-3499 update UI endpoint/settings (#7385) 2022-08-01 14:44:02 +08:00
Rex Wang
9342ba9792 EE-3502 update page docker/host/browse and docker/volume/browse (#7388) 2022-08-01 14:13:58 +08:00
Matt Hook
2552eb5e25 feat(kube): create namespace from form view [EE-3479] (#7260)
Restyle create namespace from form view
2022-08-01 16:45:28 +12:00
Matt Hook
ddaf9dc885 feat(kube): create namespace from manifest view [EE-3479] (#7306)
Restyle create from manifest
2022-08-01 16:44:56 +12:00
Richard Wei
11c778cfeb import react2angular for used by icon, tooltip and tableheader (#7391) 2022-08-01 14:59:00 +12:00
Ali
11dffdee9a feat(ui): update dashboard table & items EE-3474 (#7351) 2022-08-01 13:29:49 +12:00
Richard Wei
d4d80ed8f7 feat(ui): ui change for create access token EE-3541 (#7366)
* ui change for create access token
2022-08-01 10:08:45 +12:00
Zhang Hao
0ba10b44ec feat(secret): secret item page [EE-3511] (#7356) 2022-07-31 20:15:12 +08:00
Richard Wei
0f617f7f87 fix js console error for access control and stack page (#7347) 2022-07-29 19:11:03 +12:00
Richard Wei
423dd5e394 feat(ui): portainer new ui for homepage EE-3554 (#7328)
* add icon to homepage
2022-07-29 16:13:02 +12:00
congs
44737029a9 fix(gpu): EE-3743 gpus null error (#7342) 2022-07-29 16:08:17 +12:00
Prabhat Khera
ce22544c60 feature(ui): UI security constraints screen EE-3706 (#7314) 2022-07-29 14:41:33 +12:00
Matt Hook
9106e74e61 restyle the web editor (#7333) 2022-07-29 12:54:17 +12:00
fhanportainer
6c57ddb563 feat(ui): EE-3574 css portainer users (#7295)
* feat(ui): EE-3574 css portainer users

* feat(users): updated UI based on PR feedback

* feat(user): updated admin toggle with <por-switch-field>

* feat(user): fixed alert circle position
2022-07-29 12:45:37 +12:00
Dakota Walsh
a2e1570162 feat(ui): volume detals UI improvements EE-3483 (#7329) 2022-07-29 11:43:37 +12:00
Chaim Lev-Ari
ea60740d48 fix(sidebar): save sidebar state to local storage (#7207) 2022-07-28 14:24:25 -03:00
Chaim Lev-Ari
762c664948 feat(edge): create edge device with wizard [EE-3096] (#7029) 2022-07-28 10:34:22 -03:00
Ali
d574a71cb1 feat(ui): allow-different-modal-icons EE-3751 (#7299)
* feat(ui): update modal icons EE-3751
2022-07-28 17:33:21 +12:00
Prabhat Khera
bb066cd58c fix(ui): certificate fields fixed EE-3692 (#7336) 2022-07-28 14:41:26 +12:00
Prabhat Khera
e779939ae1 feature(ui): ui improvements kube config add from EE-3471 (#7341) 2022-07-28 11:17:32 +12:00
Richard Wei
aa830a0e58 fix(ui): fix docker images page error on pageheader EE-3668 (#7212)
* fix docker images page error with link on page-header
2022-07-28 09:53:19 +12:00
matias-portainer
52ac54f15c feat(ui): renovate edge devices list page EE-3622 (#7210) 2022-07-27 17:09:44 -03:00
matias-portainer
cc0ab75aca feat(ui): renovate the edge devices create page EE-3620 (#7221) 2022-07-27 11:19:23 -03:00
matias-portainer
7e3347da2b feat(ui): renovate the FDO devices list EE-3669 (#7231) 2022-07-27 10:47:38 -03:00
matias-portainer
87e9d7f8d4 feat(ssl): use ECDSA instead of RSA to generate the self-signed certificates EE-3097 (#6891) 2022-07-27 10:46:21 -03:00
Rex Wang
6d3a33635d EE-3694 update UI of docker/custom template (#7345) 2022-07-27 21:04:31 +08:00
Rex Wang
090268d7b6 EE-3485 update ui of docker template (#7339) 2022-07-27 20:23:33 +08:00
Rex Wang
698a91596e EE-3498 update registry/endpoint registry/manage access (#7353) 2022-07-27 20:22:40 +08:00
Ali
bb447bb02a fix(ui): remove unwanted icon hover fill EE-3737 (#7284) 2022-07-27 14:11:54 +12:00
Zhang Hao
5ffcbe8677 refactor(service): docker service edition page [EE-3520] (#7327) 2022-07-27 09:55:16 +08:00
Richard Wei
ac6296b86d feat(ui): portainer settings page ui EE-3566 (#7259)
* settings page ui change
2022-07-27 13:05:25 +12:00
Zhang Hao
3239a61bda refactor(container): container creation page and plus button [EE-3744] (#7325) 2022-07-27 07:20:21 +08:00
Zhang Hao
2a43285593 feat(docker/component/button-selector): change button selector style and remove button style [EE-3491] (#7315) 2022-07-27 07:18:06 +08:00
Richard Wei
36071837cb feat(ui): portainer login page ui EE-3542 (#7244)
* ui change for login page

* Update app/portainer/views/auth/auth.html

Co-authored-by: itsconquest <william.conquest@portainer.io>

* remove inline styles logout view

Co-authored-by: itsconquest <william.conquest@portainer.io>
2022-07-27 11:02:41 +12:00
Ali
1ef713d80b feat(ui): custom template item EE-3738 (#7303) 2022-07-27 09:40:22 +12:00
Chaim Lev-Ari
82b848af0c refactor(azure): migrate module to react [EE-2782] (#6689)
* refactor(azure): migrate module to react [EE-2782]

* fix(azure): remove optional chain

* feat(azure): apply new icons in dashboard

* feat(azure): apply new icons in dashboard

* feat(ui): allow single string for breadcrumbs

* refactor(azure/containers): use Table.content

* feat(azure/containers): implement new ui [EE-3538]

* fix(azure/containers): use correct icon

* chore(tests): mock svg as component

* fix(azure): fix tests

Co-authored-by: matias.spinarolli <matias.spinarolli@portainer.io>
2022-07-26 16:44:08 -03:00
LP B
b059641c80 fix(app/environment): console errors related to usage of React components [EE-3760] (#7310) 2022-07-26 17:50:49 +02:00
matias-portainer
728e885b9d fix(edge): restore search bar to app templates page EE-2522 (#7313) 2022-07-26 11:04:56 -03:00
congs
3acefba069 feat(ui): EE-3540 portainer-account (#7177) 2022-07-26 17:17:54 +12:00
Ali
9205f67791 feat(ui): kubernetes-configurations-list EE-3470 (#7285)
* feat(ui): configmaps/secrets table EE-3470

* feat(ui): conditionally show parent EE-3470
2022-07-26 17:12:02 +12:00
Zhang Hao
6d95643a68 refactor(service): docker service creation page [EE-3519] (#7326) 2022-07-26 07:04:01 +08:00
congs
149c414d08 fix(permission): EE-3772 Team leaders are able to see all environments (#7331) 2022-07-26 11:02:25 +12:00
matias-portainer
f8b4663e0a feat(ui): renovate the edge jobs list page EE-3532 (#7187) 2022-07-25 13:28:58 -03:00
Chaim Lev-Ari
7b774c702d fix(app): add style for be-indicator (#7140) 2022-07-25 13:24:54 -03:00
andres-portainer
8045a15a50 feat(ui): renovate the edge stacks list page EE-3535 (#7189) 2022-07-25 13:14:15 -03:00
Richard Wei
9a18dd8162 fix console error for feather icon (#7305) 2022-07-25 20:11:48 +12:00
Richard Wei
70a7eefa22 fixed cloud in ce menu (#7334) 2022-07-25 15:23:09 +12:00
Rex Wang
3356d1abe2 fix(UI) Update docker container inspect,log,stats,console,attach pages EE-3492 (#7307)
* EE-3492 update docker container inspect,log,stats,console,attach pages

* EE-3492 bug fixing

* EE-3492 replace chart bar icon

* EE-3492 bug fix

* Update resourcePoolsDatatable.html

* Update resourcePoolsDatatable.html
2022-07-25 11:03:22 +08:00
Richard Wei
7ee8dac832 fix tooltip issue for ce (#7281)
fix tooltip issue for ce
2022-07-25 13:20:36 +12:00
Rex Wang
5b3f099f4e fix(UI) Update all network pages EE-3509 (#7324)
* EE-3509 update all network pages

* EE-3509 update access control panel and network container table
2022-07-25 07:57:18 +08:00
congs
5f5cb36df1 feat(ui): EE-3553 css-portainer-environmnets (#7193) 2022-07-25 10:39:15 +12:00
Prabhat Khera
3645ff7459 feat(ui): cluster setup page done EE-3705 (#7267) 2022-07-22 14:16:50 +12:00
Chaim Lev-Ari
9a92b97b7e fix(sidebar): show authorized links [EE-3610] (#7152) 2022-07-22 14:14:31 +12:00
matias-portainer
005c48b1ad fix(edgejobs): validate date when saving job in basic configuration (#7048) 2022-07-21 16:43:52 -03:00
itsconquest
4fb1880ddc fix(auth): correctly calculate LDAP teamsync [EE-3704] (#7293) 2022-07-21 21:29:34 +12:00
Prabhat Khera
54145ce949 fix(kubeconfig): fix kubeconfig url EE-3455 (#7282) 2022-07-21 16:59:40 +12:00
itsconquest
b040aa1e78 fix(TLS): remove file type validation [EE-3672] (#7280) 2022-07-21 16:25:23 +12:00
congs
985eef6987 feat(ui): update registries css (#7249) [EE-3562] 2022-07-21 12:28:58 +12:00
Ali
a5c3116b0c fix(build): remove build script EE-3481 (#7300) 2022-07-21 10:09:56 +12:00
matias-portainer
df381b6a33 feat(templates): remove toggle and add sorting for app templates EE-2522 (#6884) 2022-07-20 16:27:15 -03:00
Chao Geng
9223c0226a EE-3742 update tool bar / action bar / search bar / pagination (#7298)
Co-authored-by: RexWangPT <rex.wang@portainer.io>
2022-07-21 00:31:13 +08:00
Ali
314fdc850e feat(ui): kubernetes-namespace-list EE-3481 (#7276)
* feat(ui): namespace list view ui changes EE-3481
2022-07-20 15:52:00 +12:00
Zhang Hao
43bbeed141 refactor(docker/switch/component): implement new design [EE-3688] (#7239)
* refactor(docker/switch/component): implement new design [EE=3688]

* revert create volume

* revert por-switch on exec.html

* refactor(container): switch fields on container creation page and edition page

* refactor(container): switch fields on networking/secret/servicewebhook/swarmvisual

* bug fixed

* code review issues

* merge code

* fix teaser for container edition

* fix encode secret toggle bug on adding secret page

* fixed a bug for service webhook toggle
2022-07-20 08:39:44 +08:00
wheresolivia
e07253bcef fix kube namespace memory usage data-cy (#7294) 2022-07-20 10:50:54 +12:00
Matt Hook
23b9baa059 feat(icons): add more svg icons and other tweaks [EE-3721] (#7270) 2022-07-20 10:50:30 +12:00
Chaim Lev-Ari
05357ecce5 fix(edge): filtering of edge devices [EE-3210] (#7077)
* fix(edge): filtering of edge devices [EE-3210]

fixes [EE-3210]

changes:
- replaces `edgeDeviceFilter` with two filters:
	- `edgeDevice`
	- `edgeDeviceUntrusted`

these filters will only apply to the edge endpoints in the query (so it's possible to get both regular endpoints and edge devices).

if `edgeDevice` is true, will filter out edge agents which are not an edge device.
			false, will filter out edge devices

`edgeDeviceUntrusted` applies only when `edgeDevice` is true. then false (default) will hide the untrusted edge devices, true will show only untrusted edge devices.

fix(edge/job-create): retrieve only trusted endpoints + fix endpoint selector pagination limits onChange

fix(endpoint-groups): remove listing of untrusted edge envs (aka in waiting room)

refactor(endpoints): move filter to another function

feat(endpoints): separate edge filters

refactor(environments): change getEnv api

refactor(endpoints): use single getEnv

feat(groups): show error when failed loading envs

style(endpoints): remove unused endpointsByGroup

* chore(deps): update go to 1.18

* fix(endpoint): filter out untrusted by default

* fix(edge): show correct endpoints

* style(endpoints): fix typo

* fix(endpoints): fix swagger

* fix(admin): use new getEnv function

Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
2022-07-19 18:00:45 +02:00
LP B
1a8fe82821 fix(app/mustache): reuse mustache variables in templates [EE-3689] (#7286) 2022-07-19 15:38:00 +02:00
LP B
95f4db4f48 fix(app/templates): handle special characters in mustache templates [EE-3708] (#7288) 2022-07-19 14:05:18 +02:00
Rex Wang
43600083a7 EE-3723 update headers to feather icon (#7275) 2022-07-19 11:29:50 +08:00
Dmitry Salakhov
e6477b0b97 fix(users): admin can change password with any auth method (#7268) [EE-3671] 2022-07-19 11:26:34 +12:00
Prabhat Khera
6aa7fdb4f2 feat(ui): UI improvements node details screen EE-3468 (#7256) 2022-07-18 11:48:24 +12:00
congs
4997e9c7be feat(gpu) EE-3191 Add GPU support for containers (#7146) 2022-07-18 11:02:14 +12:00
Peter Maguire
f0456cbf5f fix(containers): fix incorrect grammar on recreate tooltip (#7236) 2022-07-15 17:04:41 +12:00
itsconquest
a0d349e0b3 feat(buildinfo): ability to see build info [EE-2552] (#7107)
* feat(buildinfo): ability to see build info [EE-2252]

* handle dark theme

* feat: add build info to status version

* feat: include ldflags in azure pipeline

* echo shell commands in azure build

* clean up main log

* allow tests to pass

* use data from backend

* allow clicking off modal to dismiss

* add placeholder versions

* refactor

* update button class

* fix modal displaying behind elements

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
2022-07-15 11:09:38 +12:00
Prabhat Khera
f5e774c89d feat(ui): UI improvements kube app detail EE-3463 (#7176) 2022-07-15 10:49:12 +12:00
Oscar Zhou
552d3f8a3e fix(setting): update the por switch field component property (#7257) 2022-07-15 08:27:43 +12:00
Chao Geng
39f9173956 Fix(ui):docker-templates-ui [EE-3485] (#7170)
* EE-3485 update ui

* EE-3485 update ui

* EE-3485 use feather icon to replace trash icon

Co-authored-by: RexWangPT <rex.wang@portainer.io>
2022-07-14 23:16:59 +08:00
Rex Wang
e4fc41fc94 fix(ui): update UI of docker/network/create EE-3507 (#7255)
* EE-3507 update UI of docker/network/create

* EE-3507 update all icons
2022-07-14 21:35:37 +08:00
Prabhat Khera
ce7d234cba feat(ui): ui improvements on cluster landing page EE-3467 (#7245) 2022-07-14 13:50:23 +12:00
Prabhat Khera
35701f5899 svg support in icons.tsx (#7266) 2022-07-14 13:44:56 +12:00
Matt Hook
3d4d2b50ae update wording, docker-compose to docker compose (#7233) 2022-07-14 10:40:34 +12:00
Ali
0da4e3ae63 feat(ui): kube app list ui styling EE-3464 (#7247)
* feat(ui): apply app datatable changes EE-3464
2022-07-13 21:21:26 +12:00
Richard Wei
ad7055ee01 feat(ui): turn off all teaser toggle in CE (#7227)
* turn off all teaser toggle in CE
2022-07-13 15:15:11 +12:00
Richard Wei
8076455423 added nested blue icon to widget header title (#7250)
*added nested blue icon to widget header title
2022-07-13 08:48:48 +12:00
Ali
23eca3ce80 fix(r2a): fix wizard r2a bug EE-3680 (#7241) 2022-07-12 13:15:14 +12:00
sunportainer
4cc672f902 fix(UI): update-log-viewer-ui [EE-3522] (#7202)
* fix update log viewer layout

* use por-switch in logs

Co-authored-by: Hao Zhang <hao.zhang@portainer.io>
2022-07-12 07:26:23 +08:00
Prabhat Khera
82fb5f7ac1 feat(kubernetes): UI improvements kube app create EE-3462 (#7149) 2022-07-11 14:05:23 +12:00
fhanportainer
de59ea030a feat(stack): added ui label in env var section (#7010)
* feat(stack): added ui label in env var section

* feat(stack): added ui label in env var advanced section

* feat(stack): added showHelpMessage flag

* feat(stack): show help message when stack created from web editor.
2022-07-10 00:01:51 +12:00
Matt Hook
d9be6d1724 downloaded compose file should now be called docker-compose (#7228) 2022-07-08 21:47:50 +12:00
Dakota Walsh
958a8e97e9 fix(migration): close the database before running backups EE-3627 (#7218)
* fix(migration): close the database before running backups

On certain filesystems, particuarly NTFS when a network mounted windows
file server is used to store portainer's database, you are unable to
copy the database while it is open. To fix this we simply close the
database and then re-open it after a backup.

* handle close and open errors

* dont return error on nil
2022-07-08 21:05:04 +12:00
Matt Hook
5fd202d629 update to latest compose wrapper lib (#7226) 2022-07-08 16:02:24 +12:00
LP B
768f1aa663 fix(k8s/app-templates): display moustache variables fields when deploying from app template (#7184) 2022-07-08 14:15:23 +12:00
Ali
69caa1179f fix(ui): stacks example feedback EE-3676 (#7225) 2022-07-08 13:25:39 +12:00
Richard Wei
9a2cdc4a93 feat(ui): replace boxselector with react component EE-3593 (#7215)
* replace boxselector and upload vendor icon
2022-07-08 12:57:36 +12:00
Ali
14a8b1d897 feat(ui): add sorting icon component and table header cell styling EE-3626 (#7165)
* feat(ui): add sorting icons EE-3626

feat(ui): Add react component for sorting icons

feat(ui) make component usable in angular 

* feat(ui): update angular example EE-3626
2022-07-08 01:20:33 +12:00
Richard Wei
712207e69f fix(ui): fix tooltip background color (#7211) 2022-07-07 13:31:55 +03:00
Chaim Lev-Ari
8d46692d66 refactor(ui): move datatable css from bootstrap-override [EE-3664] (#7206) 2022-07-07 07:29:46 +03:00
Oscar Zhou
3241738775 fix(gitops): show prune option only in the swarm stack (#7190) 2022-07-07 11:23:22 +12:00
Chaim Lev-Ari
ce840997bf feat(ui): sort search bar icon [EE-3663] (#7205) 2022-07-06 17:05:17 +03:00
Chaim Lev-Ari
88c4a43a19 feat(ui): add icon to button [EE-3662] (#7204) 2022-07-06 17:05:00 +03:00
Chao Geng
b4acbfc9e1 fix(registry): Add input prompt and checker in edit page [EE-2705] (#7106)
* EE-2705 restrict registry edit options for different registry type
2022-07-06 19:11:59 +08:00
Chaim Lev-Ari
8bf1c91bc9 refactor(app): redesign dashboard-item component [EE-3634] (#7175) 2022-07-06 11:23:53 +03:00
Richard Wei
a66fd78dc1 feat(ui): apply react pageheader to all pageview EE-3615 (#7178)
Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2022-07-06 09:08:45 +03:00
Chaim Lev-Ari
b004b33935 fix(sidebar): sort issues [EE-3447] (#7147) 2022-07-06 08:09:14 +03:00
Dmitry Salakhov
d32793e84e fix(users): enable manual user addition (#7198) 2022-07-06 15:47:11 +12:00
Dmitry Salakhov
fd4b515350 fix(oauth): analyze id_token for Azure [EE-2984] (#7000) 2022-07-06 13:22:57 +12:00
Chaim Lev-Ari
0cd2a4558b chore(app): fix e2e tests (#7154) 2022-07-04 12:20:46 +03:00
congs
89359a21ce fix(docker): EE-3247 Portainer should not send a host information request when host management features are disabled (#7038) 2022-07-04 17:13:15 +12:00
Richard Wei
69baa279d4 feat(ui): break css into module EE-3629 (#7180)
* break css into module and fix icon mode
2022-07-04 14:11:13 +12:00
Dmitry Salakhov
33861a834b fix(compose): merge default and in-place stack env vars [EE-2860] (#7076) 2022-07-04 13:16:04 +12:00
Matt Hook
dd4d126934 feat(switch): add optional switch text [EE-3625] (#7164)
* add optional switch text
2022-07-04 13:05:04 +12:00
Oscar Zhou
7275d23e4b feat(stack/swarm): add prune option for swarm stack redeployment [EE-2678] (#7025) 2022-07-04 11:39:03 +12:00
Chaim Lev-Ari
d7306fb22e refactor(app): replace angularjs tooltip with react [EE-3606] (#7172)
* refactor(app): replace angularjs tooltip with react
2022-07-04 11:21:25 +12:00
Dmitry Salakhov
ebc0a8c772 fix: update build scripts for mac (#7104) 2022-07-04 10:43:11 +12:00
Richard Wei
f26e1fa21b add inline-flex to button group (#7168)
* add inline-flex to button group
2022-07-04 07:16:45 +12:00
matias-portainer
6b27ba9121 fix(edge): delete endpoint proxy only when updating URL, TLS or is Edge Agent on Kubernetes EE-2759 (#7086) 2022-07-01 11:36:01 -03:00
congs
975dc9c1da fix(edge): EE-3092 hide the ability to add edge agents in Docker Desktop extension (#7090) 2022-07-01 17:22:40 +12:00
Chaim Lev-Ari
6fe26a52dd feat(app): ui additional css class [EE-3594] (#7157)
* feat(app): ui additional css class [EE-3594]
2022-07-01 13:14:22 +12:00
Chao Geng
cd66e32912 EE-2570 disable pull image toggle when invalid (#7002) 2022-06-30 08:35:32 +08:00
Prabhat Khera
81f8b88541 fix ingress published url (#7113) 2022-06-29 16:28:09 +12:00
Chaim Lev-Ari
882051cc30 chore(sidebar): add data-cys [EE-3605] (#7143)
* chore(sidebar): add data-cys [EE-3605]

fix [EE-3605]
2022-06-28 19:36:40 +03:00
Chaim Lev-Ari
ed8f9b5931 feat(sidebar): implement new design [EE-3447] (#7118) 2022-06-28 10:42:42 +03:00
Prabhat Khera
e5e57978af delete force terminating namespace (#7081) 2022-06-28 16:35:30 +12:00
Steven Kang
75fef397d3 Set static DOCKER_VERSION for ppc64le and s390x (#7136) 2022-06-28 11:40:18 +12:00
Chaim Lev-Ari
624490716e fix(environments): hide async mode on deployment [EE-3380] (#7130)
fixes [EE-3380]
2022-06-28 10:23:15 +12:00
andres-portainer
8eff32ebc7 fix(css): improve the handling of different color entries EE-3603 (#7134) 2022-06-27 18:11:14 -03:00
Chaim Lev-Ari
cd19eb036b refactor(app): use colors with tailwind [EE-3601] (#7133)
* refactor(app): use colors with tailwind
2022-06-28 07:16:28 +12:00
Chaim Lev-Ari
95f706aabe fix(analytics): load public settings [EE-3590] (#7128) 2022-06-27 19:29:17 +03:00
Ali
1551b02fde fix(home): dont close filter on select EE-3257 (#6991) 2022-06-27 13:47:07 +12:00
itsconquest
557f4773cf feat(extension): remove unused port [EE-3152] (#7075) 2022-06-27 10:27:37 +12:00
Steven Kang
b84e1c8550 Set static DOCKER_VERSION for ppc64le and s390x (#7123) 2022-06-27 09:48:49 +12:00
Chaim Lev-Ari
46e1a01625 refactor(docker): move components to react [EE-3348] (#7084) 2022-06-26 17:16:50 +03:00
Chaim Lev-Ari
7238372d8d fix(api): add missing edge types [EE-3590] (#7116) 2022-06-26 08:38:23 +03:00
andres-portainer
00126cd08a fix(wizard): replace the YAML file by the docker commands EE-3589 (#7111) 2022-06-24 14:59:10 -03:00
LP B
58c44ad1ea fix(app/account): ensure newTransition exists in uiCanExit [EE-3336] (#7110) 2022-06-24 17:35:35 +02:00
Chaim Lev-Ari
84611a90a1 refactor(sidebar): migrate sidebar to react [EE-2907] (#6725)
* refactor(sidebar): migrate sidebar to react [EE-2907]

fixes [EE-2907]

feat(sidebar): show label for help

fix(sidebar): apply changes from ddExtension

fix(sidebar): resolve conflicts

style(ts): add explanation for ddExtension

fix(sidebar): use enum for status

refactor(sidebar): rename to EdgeComputeSidebar

refactor(sidebar): removed the need of `ident` prop

style(sidebar): add ref for mobile breakpoint

refactor(app): document testing props

refactor(sidebar): use single sidebar item

refactor(sidebar): use section for nav

refactor(sidebar): rename sidebarlink to link

refactor(sidebar): memoize menu paths

fix(kubectl-shell): infinite loop on hooks dependencies

refactor(sidebar): use authorized element

feat(k8s/shell): track open shell

refactor(k8s/shell): remove memoization

refactor(settings): move settings queries to queries

fix(sidebar): close sidebar on mobile

refactor(settings): use mutation helpers

refactor(sidebar): remove memo

refactor(sidebar): rename sidebar item for storybook

refactor(sidebar): move to react

gprefactor(sidebar): remove dependence on EndProvider

feat(environments): rename settings type

feat(kube): move kubeconfig button

fix(sidebar): open submenus

fix(sidebar): open on expand

fix(sibebar): show kube shell correctly

* fix(sidebar): import from react component

* chore(tests): fix missing prop
2022-06-23 10:25:56 +03:00
Chaim Lev-Ari
f78a6568a6 feat(ui): portainer base component css change [EE-3381] (#7115) 2022-06-23 09:32:18 +03:00
Chaim Lev-Ari
825269c119 fix(edge): show heartbeat for async env [EE-3380] (#7097) 2022-06-22 20:11:46 +03:00
Chaim Lev-Ari
60cd7b5527 chore(tests): remove cypress code [EE-3580] (#7103) 2022-06-22 07:59:53 +03:00
Matt Hook
767fabe0ce fix docker download path for mac platforms (#7102) 2022-06-22 10:06:46 +12:00
matias-portainer
f86ba7b176 feat(edge): move edge jobs out of beta (#7105) 2022-06-21 17:57:59 -03:00
Hao Zhang
912250732a feat(psp): kubernetes pod security policy EE-1577 (#6553)
* docs(github): fix slack link [EE-2438] (#6541)

Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
Co-authored-by: Chao Geng <93526589+chaogeng77977@users.noreply.github.com>
Co-authored-by: chaogeng77977 <chao.geng@portainer.io>
2022-06-20 15:48:41 +08:00
itsconquest
ae731b5496 fix(auth): track skips per user [EE-3318] (#7089) 2022-06-20 17:00:07 +12:00
Chaim Lev-Ari
92eaa02156 fix(docker/networks): show correct resource control data [EE-3401] (#7060) 2022-06-17 19:21:41 +03:00
Chaim Lev-Ari
18252ab854 refactor(app): move react components to react codebase [EE-3179] (#6971) 2022-06-17 19:18:42 +03:00
itsconquest
212400c283 fix(auth): clear skips when using new instance [EE-3331] (#7027) 2022-06-17 14:45:47 +12:00
LP B
8ed41de815 fix(app/account): create access token button (#7014)
* fix(api): remove unused leftover imports

* fix(app/account): create access token button

* fix(app/formcontrol): error message overlapping input on smaller screens
2022-06-16 21:25:37 +02:00
Chaim Lev-Ari
97a880e6c1 feat(custom-templates): hide variables [EE-2602] (#7068) 2022-06-16 08:32:41 +03:00
itsconquest
f39775752d feat(auth): allow single char passwords [EE-3385] (#7050)
* feat(auth): allow single character passwords

* match weak password modal logic to slider
2022-06-16 12:31:36 +12:00
Matt Hook
6d6c70a98b fix(swarm): don't stomp on the x-registry-auth header EE-3308 (#7080)
* don't stomp on the x-registry-auth header

* del header if empty json provided for registry auth
2022-06-16 09:53:58 +12:00
Dmitry Salakhov
461fc91446 fix: clarify password change error (#7082) 2022-06-15 16:56:59 +12:00
itsconquest
8059cae8e7 fix(auth): notify user password requirements [EE-3344] (#7042)
* fix(auth): notify user password requirements [EE-3344]

* fix angular code
2022-06-15 16:01:19 +12:00
congs
41107191c3 fix(teamleader): EE-3411 normal users get an unauthorized error (#7052) 2022-06-14 14:12:25 +12:00
sunportainer
cb6a5fa41d fix(typo):UI and logs EE-3282 (#7063)
* fix logs and UI typos
2022-06-13 14:53:51 +08:00
Ali
66799a53f4 fix(wizard): return back to envs page EE-3419 (#7065) 2022-06-13 14:59:41 +12:00
congs
892fdbf60d fix(teamleader): EE-3383 allow teamleader promote member to teamleader (#7040) 2022-06-10 17:13:33 +12:00
Chao Geng
b6309682ef feat(kubeconfig): pagination for downloading kubeconfigs EE-2141 (#6895)
* EE-2141 Add pagination to kubeconfig download dialog
2022-06-10 11:42:27 +08:00
Ali
be11dfc231 fix(wizard): show teasers for kaas and kubeconfig features [EE-3316] (#7008)
* fix(wizard): add kubeconfig, nomad and kaas teasers
2022-06-10 09:17:13 +12:00
congs
12527aa820 fix(teamleader): EE-3332 hide name and leaders (#7031) 2022-06-09 14:22:35 +12:00
Matt Hook
0d0f9499eb chore(version): fix readme version (#7028) 2022-06-08 23:20:58 +12:00
Ali
60eab3e263 fix(wizard): use 'New Environments' title EE-3329 (#7034) 2022-06-08 16:35:58 +12:00
Chao Geng
eb547162e9 fix(image) add validation of image name in build image page [EE-3010] (#6988)
* EE-3010 add validation of image name
2022-06-07 16:42:09 +08:00
Matt Hook
0864c371e8 chore(version): bump develop branch version to 2.15 (#7019)
* bump version to 2.15
2022-06-07 11:00:36 +12:00
Chaim Lev-Ari
b90b1701e9 fix(users): remove unused imports [EE-3340] (#7016)
fixes [EE-3340]
2022-06-06 10:04:33 +03:00
Ali
eb4ff12744 feat(wizard): replace-the-add-envs-button-with-env-wizard-button EE-3001 (#7013)
* feat(envs): on env click, direct user to wizard
2022-06-03 22:33:17 +12:00
congs
0522032515 feat(teamleader) EE-294 redesign team leader (#6973)
feat(teamleader) EE-294 redesign team leader (#6973)
2022-06-03 16:44:42 +12:00
itsconquest
bca1c6b9cf feat(internal-auth): ability to set minimum password length [EE-3175] (#6942)
* feat(internal-auth): ability to set minimum password length [EE-3175]

* pass props to react component

* fixes + WIP slider

* fix slider updating + add styles

* remove nested ternary

* fix slider updating + add remind me later button

* add length to settings + value & onchange method

* finish my account view

* fix slider updating

* slider styles

* update style

* move slider in

* update size of slider

* allow admin to browse to authentication view

* use feather icons instead of font awesome

* feat(settings): add colors to password rules

* clean up tooltip styles

* more style changes

* styles

* fixes + use requiredLength in password field for icon logic

* simplify logic

* simplify slider logic and remove debug code

* use required length for logic to display pwd length warning

* fix slider styles

* use requiredPasswordLength to determine if password is valid

* style tooltip based on theme

* reset skips when password is changed

* misc cleanup

* reset skips when required length is changed

* fix formatting

* fix issues

* implement some suggestions

* simplify logic

* update broken test

* pick min password length from DB

* fix suggestions

* set up min password length in the DB

* fix test after migration

* fix formatting issue

* fix bug with icon

* refactored migration

* fix typo

* fixes

* fix logic

* set skips per user

* reset skips for all users on length change

Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
2022-06-03 16:00:13 +12:00
Matt Hook
4195d93a16 fix typo 2022-06-03 14:21:55 +12:00
Matt Hook
e8a8b71daa feat(compose): upgrade to docker compose v2 EE-2096 (#6994)
Upgrade to compose v2 + new helm + new kubectl
2022-06-03 13:50:37 +12:00
Ali
aea62723c0 fix(forms): increase-click-area-for-expandable-form-section EE-3314 (#7007)
* fix(forms): increase click area for form section
2022-06-03 13:29:23 +12:00
Prabhat Khera
9b58c2e466 rename output_35 to output_24_to_latest (#7006) 2022-06-02 11:30:42 +12:00
Prabhat Khera
c41f7f8270 chore(version): version bump to 2.14.0 (#6958) 2022-06-02 10:53:48 +12:00
Chaim Lev-Ari
ac096dda46 feat(wizard): add edge form [EE-3000] (#6979) 2022-06-01 07:28:31 +03:00
Chaim Lev-Ari
e686d64011 refactor(docker): strongly type snapshot [EE-3256] (#6990)
* refactor(docker): strongly type snapshot [EE-3256]

fixes [EE-3256]

* fix(endpoints): return empty from association api

* refactor(docker): ignore raw snapshot for swagger
2022-05-31 13:03:10 +03:00
Chaim Lev-Ari
1ccdb64938 refactor(custom-templates): render template variables [EE-2602] (#6937) 2022-05-31 13:00:47 +03:00
Prabhat Khera
71c0e8e661 fix(kubernetes): fix redeploying kubernetes app EE-2875 (#6984) 2022-05-31 10:12:37 +12:00
andres-portainer
c162e180e0 fix(endpoints): remove global map to avoid panic writes EE-3160 (#6918) 2022-05-30 11:22:37 -03:00
Ali
e806f74652 refactor(tailwind): add-consistent-theme-colors-to-tailwind EE-3255 (#6989)
* refactor(tailwind): add custom colors EE-3255
2022-05-30 14:01:05 +12:00
Chaim Lev-Ari
d52417c14f refactor(app): convert tag-selector to react [EE-2983] (#6783) 2022-05-29 09:14:14 +03:00
Chaim Lev-Ari
75d854e6ad Revert "refactor(docker): strongly type snapshot [EE-3256]"
This reverts commit 0b2217a916.
2022-05-26 15:39:55 +03:00
Chaim Lev-Ari
0b2217a916 refactor(docker): strongly type snapshot [EE-3256]
fixes [EE-3256]
2022-05-26 15:34:34 +03:00
Chao Geng
ca30efeca7 EE-1892 Centralize prompt dialog (#6903) 2022-05-24 20:14:38 +08:00
Chaim Lev-Ari
dc98850489 feat(app): enforce using of props in r2a [EE-3215] (#6943) 2022-05-24 08:35:20 +03:00
Chaim Lev-Ari
01dc9066b7 refactor(wizard): migrate to react [EE-2305] (#6957) 2022-05-23 17:32:51 +03:00
Chao Geng
3aacaa7caf feat(dashboard) remove environment url from dashboard EE-2849 (#6955)
* EE-2849 remove environment url from dashboard

* EE-2849 only remove edge env's url

* EE-2849 remove logging
2022-05-23 17:05:37 +08:00
Chaim Lev-Ari
b031a30f62 feat(edge-devices): set specific page to view [EE-2082] (#6869) 2022-05-23 10:57:22 +03:00
Chaim Lev-Ari
12cddbd896 feat(demo): disable features on demo env [EE-1874] (#6040) 2022-05-22 08:34:09 +03:00
Chao Geng
3791b7a16f fix(kube): misspelling kube namespace (#6951) 2022-05-20 07:34:30 +08:00
matias-portainer
d754532ab1 chore(edgestacks): add unit tests for edge stacks (#6931)
chore(edgestacks): add unit tests for edge stacks EE-3172
2022-05-19 17:13:51 -03:00
Chao Geng
9a48ceaec1 fix(docker): Restrict registry edit options for different registry type EE-2705 (#6708)
* EE-2705 restrict registry edit options for different registry type

* EE-2705 quay and azure registry should not disable authentication

* EE-2705 Resolve conflict
2022-05-18 18:46:24 +08:00
Chaim Lev-Ari
1132c9ce87 refactor(app): create empty react structure [EE-3178] (#6926) 2022-05-17 07:22:44 +03:00
itsconquest
668d526604 fix(networks): handle windows specific system networks [EE-2594] (#6922) 2022-05-17 14:45:30 +12:00
Chaim Lev-Ari
0e257c200f chore(app): use base font-size of 16px [EE-3186] (#6938) 2022-05-16 10:24:13 +03:00
congs
df05914fac fix(git) EE-2026 git default branch (#6876)
fix(git) EE-2026 git default branch
2022-05-16 09:35:11 +12:00
Chaim Lev-Ari
0ffb84aaa6 refactor(app): add rq mutation helpers [EE-3176] (#6923) 2022-05-15 10:01:08 +03:00
Chaim Lev-Ari
b01180bb29 chore(deps): remove lodash-es dependency [EE-2560] (#6576) 2022-05-12 08:44:53 +03:00
cong meng
16f8b737f1 fix(pwd) EE-3161 ease the minimum password restrictions to 12 characters (#6921)
* fix(pwd): EE-3161 ease the minimum password restrictions to 12 characters
2022-05-12 13:17:01 +12:00
itsconquest
d9d1d6bfaa feat(extension): add a readme [EE-3085] (#6888)
* feat(extension): add a readme [EE-3085]

* add prerequisites
2022-05-11 11:58:11 +12: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
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
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
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
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
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
Chaim Lev-Ari
389561eb28 fix(registries): sync code with ee [EE-2176] (#6355)
fixes [EE-2176]
2022-01-11 07:35:09 +02:00
Dmitry Salakhov
bc54d687be refactor: unit tests (#6367) 2022-01-11 10:26:41 +13:00
Chaim Lev-Ari
8e45076f35 feat(i18n): add support for multiple languages (#6270)
feat(users): add i18n to create access token

chore(app): remove test code
2022-01-10 15:22:21 +02:00
Chaim Lev-Ari
87dda810fc fix(edgestacks): create new stack [EE-2178] (#6311)
* fix(edgestacks): create new stack [EE-2178]

[EE-2178]

* refactor(edgestacks): id is required on create
2022-01-10 11:36:46 +02:00
Chao Geng
4e77d2d772 fix(download-plugin): Image name not available when using watchtower or similar (#6225)
* make plugin version 1.0.22 and correct download-file name

* updated to v2.0.0-rc.2

* rollback download_docker_compose_binary.sh
2022-01-10 10:07:46 +08:00
Dmitry Salakhov
0b62a3d664 feat: bump golang version to 1.17.6 (#6366) 2022-01-10 13:10:02 +13:00
Richard Wei
84f354452b feat(k8s): add ingressClassName to payload EE-2129 (#6265)
* add ingressClassName to payload

* add IngressClass.Name into formValues
2022-01-10 09:02:02 +13:00
Chaim Lev-Ari
c24d8fab0f chore(tests): update AccessControlForm snapshots [EE-2348] (#6361) 2022-01-07 12:14:36 -03:00
Chaim Lev-Ari
5362e15624 fix(ldap): show BE border correctly (#6357) 2022-01-07 12:58:15 +02:00
Chaim Lev-Ari
07c6ce84c2 refactor(environments): remove angular dep from service [EE-2346] (#6360)
refactor(environments): parse axios error
2022-01-06 18:31:47 +02:00
Chaim Lev-Ari
ecd0eb6170 refactor(app): create access-control-form react component [EE-2332] (#6346)
* refactor(app): create access-control-form react component [EE-2332]

fix [EE-2332]

* chore(tests): setup msw for async tests and stories

chore(sb): add msw support for storybook

* refactor(access-control): move loading into component

* fix(app): fix users and teams selector stories

* chore(access-control): write test for validation
2022-01-05 18:28:56 +02:00
Marcelo Rydel
8dbb802fb1 feat(react): add FileUploadField and FileUploadForm components [EE-2336] (#6350) 2022-01-05 10:39:34 -03:00
Chaim Lev-Ari
07e7fbd270 refactor(containers): replace containers datatable with react component [EE-1815] (#6059) 2022-01-04 14:16:09 +02:00
fhanportainer
65821aaccc feat(react): migrate analytics interface to react. (#6296) [EE-2100] 2022-01-03 17:49:59 +02:00
Chaim Lev-Ari
d33ac8c588 refactor(app): create a composed header component [EE-2329] (#6326)
* refactor(app): create a composed header component

refactor(app): support single child breadcrumbs

fix(app): fix breadcrumbs warning

* refactor(app): import breadcrumbs

* refactor(app): support object breadcrumbs

* chore(app): write tests for header components
2021-12-30 16:46:12 +01:00
Marcelo Rydel
102a07346a fix(kubeconfig): fix modal inputType [EE-2325] (#6317) 2021-12-23 10:44:56 -03:00
Chaim Lev-Ari
8fc5a5e8a1 fix(teams): create more then one team [EE-2184] (#6305)
fixes [EE-2184]
2021-12-23 07:57:32 +02:00
andres-portainer
cdfa9b25a8 fix(home): display tags properly [EE-2153] (#6275)
fix(home): display tags properly EE-2153
2021-12-22 19:39:23 -03:00
Richard Wei
e7fc996424 fix scroolbar shown in confirmation dialogs (#6264) 2021-12-22 11:32:04 +08:00
sunportainer
1c374b9fd2 Fix(UI): disable autofill username input EE-2140 (#6252)
* fix/ee-2140/disable-autofill-username
2021-12-22 10:34:55 +08:00
Chaim Lev-Ari
d9db789511 chore(build): add script to analyze webpack bundle [EE-2132] (#6259)
* chore(build): add script to analyze webpack bundle

* chore(build): use single dep (lodash,moment)
2021-12-21 14:32:48 +02:00
Chaim Lev-Ari
5a3687a564 fix(app): main services [EE-1896] (#6279)
[EE-1896]
2021-12-21 12:08:44 +02:00
Chao Geng
6e53bf5dc7 support upgrading (#6256) 2021-12-21 08:45:05 +08:00
Chaim Lev-Ari
e25141d899 fix(modals): upgrade jquery versions (#6303) 2021-12-21 11:51:48 +13:00
Chaim Lev-Ari
4f7b432f44 feat(app): introduce form framework [EE-1946] (#6272) 2021-12-20 19:21:19 +02:00
2551 changed files with 93708 additions and 45599 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

@@ -31,7 +31,12 @@ rules:
[
'error',
{
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
pathGroups:
[
{ pattern: '@@/**', group: 'internal', position: 'after' },
{ pattern: '@/**', group: 'internal' },
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroupsExcludedImportTypes: ['internal'],
},
@@ -41,6 +46,7 @@ settings:
'import/resolver':
alias:
map:
- ['@@', './app/react/components']
- ['@', './app']
extensions: ['.js', '.ts', '.tsx']
@@ -52,6 +58,7 @@ overrides:
parser: '@typescript-eslint/parser'
plugins:
- '@typescript-eslint'
- 'regex'
extends:
- airbnb
- airbnb-typescript
@@ -68,7 +75,14 @@ overrides:
version: 'detect'
rules:
import/order:
['error', { pathGroups: [{ pattern: '@/**', group: 'internal' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
[
'error',
{
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { pattern: '@/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
},
]
func-style: [error, 'declaration']
import/prefer-default-export: off
no-use-before-define: ['error', { functions: false }]
@@ -79,6 +93,7 @@ overrides:
react/forbid-prop-types: off
react/require-default-props: off
react/no-array-index-key: off
no-underscore-dangle: off
react/jsx-filename-extension: [0]
import/no-extraneous-dependencies: ['error', { devDependencies: true }]
'@typescript-eslint/explicit-module-boundary-types': off
@@ -87,6 +102,9 @@ overrides:
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either' }]
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
'react/jsx-no-bind': off
'no-await-in-loop': 'off'
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
'regex/invalid': ['error', [{ 'regex': 'data-feather="(.*)"', 'message': 'Please use `react-feather` package instead' }]]
- files:
- app/**/*.test.*
extends:
@@ -94,3 +112,9 @@ overrides:
- 'plugin:jest/style'
env:
'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 ./...

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@ storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json
.vscode
*.DS_Store
.eslintcache

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'),
},
},
},
],
@@ -31,4 +34,5 @@ module.exports = {
core: {
builder: 'webpack5',
},
staticDirs: ['./public'],
};

View File

@@ -1,6 +1,23 @@
import '../app/assets/css';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswDecorator } from 'msw-storybook-addon';
import { handlers } from '@/setup-tests/server-handlers';
import { QueryClient, QueryClientProvider } from 'react-query';
// Initialize MSW
initMSW({
onUnhandledRequest: ({ method, url }) => {
if (url.pathname.startsWith('/api')) {
console.error(`Unhandled ${method} request to ${url}.
This exception has been only logged in the console, however, it's strongly recommended to resolve this error as you don't want unmocked data in Storybook stories.
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses
`);
}
},
});
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
@@ -10,12 +27,22 @@ export const parameters = {
date: /Date$/,
},
},
msw: {
handlers,
},
};
const testQueryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
export const decorators = [
(Story) => (
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
<QueryClientProvider client={testQueryClient}>
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
</QueryClientProvider>
),
mswDecorator,
];

View File

@@ -0,0 +1,328 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker (0.36.3).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const INTEGRITY_CHECKSUM = '02f4ad4a2797f85668baf196e553d929';
const bypassHeaderName = 'x-msw-bypass';
const activeClientIds = new Set();
self.addEventListener('install', function () {
return self.skipWaiting();
});
self.addEventListener('activate', async function (event) {
return self.clients.claim();
});
self.addEventListener('message', async function (event) {
const clientId = event.source.id;
if (!clientId || !self.clients) {
return;
}
const client = await self.clients.get(clientId);
if (!client) {
return;
}
const allClients = await self.clients.matchAll();
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
});
break;
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: INTEGRITY_CHECKSUM,
});
break;
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId);
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: true,
});
break;
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId);
break;
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId);
const remainingClients = allClients.filter((client) => {
return client.id !== clientId;
});
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister();
}
break;
}
}
});
// Resolve the "main" client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId);
if (client.frameType === 'top-level') {
return client;
}
const allClients = await self.clients.matchAll();
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible';
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id);
});
}
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event);
const response = await getResponse(event, client, requestId);
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
(async function () {
const clonedResponse = response.clone();
sendToClient(client, {
type: 'RESPONSE',
payload: {
requestId,
type: clonedResponse.type,
ok: clonedResponse.ok,
status: clonedResponse.status,
statusText: clonedResponse.statusText,
body: clonedResponse.body === null ? null : await clonedResponse.text(),
headers: serializeHeaders(clonedResponse.headers),
redirected: clonedResponse.redirected,
},
});
})();
}
return response;
}
async function getResponse(event, client, requestId) {
const { request } = event;
const requestClone = request.clone();
const getOriginalResponse = () => fetch(requestClone);
// Bypass mocking when the request client is not active.
if (!client) {
return getOriginalResponse();
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return await getOriginalResponse();
}
// Bypass requests with the explicit bypass header
if (requestClone.headers.get(bypassHeaderName) === 'true') {
const cleanRequestHeaders = serializeHeaders(requestClone.headers);
// Remove the bypass header to comply with the CORS preflight check.
delete cleanRequestHeaders[bypassHeaderName];
const originalRequest = new Request(requestClone, {
headers: new Headers(cleanRequestHeaders),
});
return fetch(originalRequest);
}
// Send the request to the client-side MSW.
const reqHeaders = serializeHeaders(request.headers);
const body = await request.text();
const clientMessage = await sendToClient(client, {
type: 'REQUEST',
payload: {
id: requestId,
url: request.url,
method: request.method,
headers: reqHeaders,
cache: request.cache,
mode: request.mode,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body,
bodyUsed: request.bodyUsed,
keepalive: request.keepalive,
},
});
switch (clientMessage.type) {
case 'MOCK_SUCCESS': {
return delayPromise(() => respondWithMock(clientMessage), clientMessage.payload.delay);
}
case 'MOCK_NOT_FOUND': {
return getOriginalResponse();
}
case 'NETWORK_ERROR': {
const { name, message } = clientMessage.payload;
const networkError = new Error(message);
networkError.name = name;
// Rejecting a request Promise emulates a network error.
throw networkError;
}
case 'INTERNAL_ERROR': {
const parsedBody = JSON.parse(clientMessage.payload.body);
console.error(
`\
[MSW] Uncaught exception in the request handler for "%s %s":
${parsedBody.location}
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error, as it indicates a mistake in your code. If you wish to mock an error response, please see this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
`,
request.method,
request.url
);
return respondWithMock(clientMessage);
}
}
return getOriginalResponse();
}
self.addEventListener('fetch', function (event) {
const { request } = event;
const accept = request.headers.get('accept') || '';
// Bypass server-sent events.
if (accept.includes('text/event-stream')) {
return;
}
// Bypass navigation requests.
if (request.mode === 'navigate') {
return;
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return;
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return;
}
const requestId = uuidv4();
return event.respondWith(
handleRequest(event, requestId).catch((error) => {
if (error.name === 'NetworkError') {
console.warn('[MSW] Successfully emulated a network error for the "%s %s" request.', request.method, request.url);
return;
}
// At this point, any exception indicates an issue with the original request/response.
console.error(
`\
[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`,
request.method,
request.url,
`${error.name}: ${error.message}`
);
})
);
});
function serializeHeaders(headers) {
const reqHeaders = {};
headers.forEach((value, name) => {
reqHeaders[name] = reqHeaders[name] ? [].concat(reqHeaders[name]).concat(value) : value;
});
return reqHeaders;
}
function sendToClient(client, message) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error);
}
resolve(event.data);
};
client.postMessage(JSON.stringify(message), [channel.port2]);
});
}
function delayPromise(cb, duration) {
return new Promise((resolve) => {
setTimeout(() => resolve(cb()), duration);
});
}
function respondWithMock(clientMessage) {
return new Response(clientMessage.payload.body, {
...clientMessage.payload,
headers: clientMessage.payload.headers,
});
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

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

@@ -12,21 +12,15 @@ Portainer consists of a single container that can run on any cluster. It can be
- [Take5 get 5 free nodes of Portainer Business for as long as you want them](https://portainer.io/pricing/take5)
- [Portainer BE install guide](https://install.portainer.io)
## Demo
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
Please note that the public demo cluster is **reset every 15min**.
## Latest Version
Portainer CE is updated regularly. We aim to do an update release every couple of months.
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
[![latest version](https://img.shields.io/github/v/release/portainer/portainer?color=%2344cc11&label=Latest%20release&style=for-the-badge)](https://github.com/portainer/portainer/releases/latest)
## Getting started
- [Deploy Portainer](https://docs.portainer.io/v/ce-2.9/start/install)
- [Deploy Portainer](https://docs.portainer.io/start/install)
- [Documentation](https://documentation.portainer.io)
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
@@ -42,10 +36,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

@@ -2,57 +2,83 @@ 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"
"github.com/rs/zerolog/log"
)
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
go func() {
log.Println("[DEBUG] [internal,init] [message: start initialization monitor ]")
log.Debug().Msg("start initialization monitor")
select {
case <-time.After(m.timeout):
initialized, err := m.WasInitialized()
if err != nil {
logFatalf("%s", err)
log.Fatal().Err(err).Msg("")
}
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.Info().Msg("the Portainer instance timed out for security purposes, to re-enable your Portainer instance, you will need to restart Portainer")
m.mu.Lock()
defer m.mu.Unlock()
m.adminInitDisabled = true
return
}
case <-cancellationCtx.Done():
log.Println("[DEBUG] [internal,init] [message: canceling initialization monitor]")
log.Debug().Msg("canceling initialization monitor")
case <-m.shutdownCtx.Done():
log.Println("[DEBUG] [internal,init] [message: shutting down initialization monitor]")
log.Debug().Msg("shutting down initialization monitor")
}
}()
}
// 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 +94,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")
}

71
api/agent/version.go Normal file
View File

@@ -0,0 +1,71 @@
package agent
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"strconv"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/url"
)
// GetAgentVersionAndPlatform returns the agent version and platform
//
// it sends a ping to the agent and parses the version and platform from the headers
func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) {
httpCli := &http.Client{
Timeout: 3 * time.Second,
}
if tlsConfig != nil {
httpCli.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
parsedURL, err := url.ParseURL(endpointUrl + "/ping")
if err != nil {
return 0, "", err
}
parsedURL.Scheme = "https"
req, err := http.NewRequest(http.MethodGet, parsedURL.String(), nil)
if err != nil {
return 0, "", err
}
resp, err := httpCli.Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
}
version := resp.Header.Get(portainer.PortainerAgentHeader)
if version == "" {
return 0, "", errors.New("Version Header is missing")
}
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
if agentPlatformHeader == "" {
return 0, "", errors.New("Agent Platform Header is missing")
}
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
if err != nil {
return 0, "", err
}
if agentPlatformNumber == 0 {
return 0, "", errors.New("Agent platform is invalid")
}
return portainer.AgentPlatform(agentPlatformNumber), version, nil
}

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

@@ -2,7 +2,6 @@ package apikey
import (
"crypto/sha256"
"log"
"strings"
"testing"
"time"
@@ -10,6 +9,8 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
"github.com/rs/zerolog/log"
)
func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
@@ -20,7 +21,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(t, true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -74,7 +75,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(t, true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -94,7 +95,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(t, true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -115,7 +116,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(t, true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -151,7 +152,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(t, true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -169,8 +170,8 @@ func Test_UpdateAPIKey(t *testing.T) {
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
log.Println(apiKey)
log.Println(apiKeyGot)
log.Debug().Msgf("%+v", apiKey)
log.Debug().Msgf("%+v", apiKeyGot)
is.Equal(apiKey.LastUsed, apiKeyGot.LastUsed)
@@ -199,7 +200,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(t, true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
@@ -240,7 +241,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(t, true, true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())

View File

@@ -34,3 +34,45 @@ func TarFileInBuffer(fileContent []byte, fileName string, mode int64) ([]byte, e
return buffer.Bytes(), nil
}
// tarFileInBuffer represents a tar archive buffer.
type tarFileInBuffer struct {
b *bytes.Buffer
w *tar.Writer
}
func NewTarFileInBuffer() *tarFileInBuffer {
var b bytes.Buffer
return &tarFileInBuffer{
b: &b,
w: tar.NewWriter(&b),
}
}
// Put puts a single file to tar archive buffer.
func (t *tarFileInBuffer) Put(fileContent []byte, fileName string, mode int64) error {
hdr := &tar.Header{
Name: fileName,
Mode: mode,
Size: int64(len(fileContent)),
}
if err := t.w.WriteHeader(hdr); err != nil {
return err
}
if _, err := t.w.Write(fileContent); err != nil {
return err
}
return nil
}
// Bytes returns the archive as a byte array.
func (t *tarFileInBuffer) Bytes() []byte {
return t.b.Bytes()
}
func (t *tarFileInBuffer) Close() error {
return t.w.Close()
}

View File

@@ -9,7 +9,6 @@ import (
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
"github.com/stretchr/testify/assert"
)
@@ -27,9 +26,7 @@ func listFiles(dir string) []string {
}
func Test_shouldCreateArhive(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
tmpdir := t.TempDir()
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
@@ -40,9 +37,7 @@ func Test_shouldCreateArhive(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir, _ := ioutils.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
extractionDir := t.TempDir()
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
err = cmd.Run()
if err != nil {
@@ -63,9 +58,7 @@ func Test_shouldCreateArhive(t *testing.T) {
}
func Test_shouldCreateArhiveXXXXX(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "backup")
defer os.RemoveAll(tmpdir)
tmpdir := t.TempDir()
content := []byte("content")
ioutil.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
@@ -76,9 +69,7 @@ func Test_shouldCreateArhiveXXXXX(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, fmt.Sprintf("%s.tar.gz", filepath.Base(tmpdir))), gzPath)
extractionDir, _ := ioutils.TempDir("", "extract")
defer os.RemoveAll(extractionDir)
extractionDir := t.TempDir()
r, _ := os.Open(gzPath)
ExtractTarGz(r, extractionDir)
if err != nil {

View File

@@ -2,16 +2,12 @@ package archive
import (
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"path/filepath"
"testing"
)
func TestUnzipFile(t *testing.T) {
dir, err := ioutil.TempDir("", "unzip-test-")
assert.NoError(t, err)
defer os.RemoveAll(dir)
dir := t.TempDir()
/*
Archive structure.
├── 0
@@ -21,7 +17,7 @@ func TestUnzipFile(t *testing.T) {
└── 0.txt
*/
err = UnzipFile("./testdata/sample_archive.zip", dir)
err := UnzipFile("./testdata/sample_archive.zip", dir)
assert.NoError(t, err)
archiveDir := dir + "/sample_archive"

View File

@@ -7,13 +7,14 @@ import (
"path/filepath"
"time"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/sirupsen/logrus"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
const rwxr__r__ os.FileMode = 0744
@@ -47,9 +48,9 @@ func CreateBackupArchive(password string, gate *offlinegate.OfflineGate, datasto
err := datastore.Export(exportFilename)
if err != nil {
logrus.WithError(err).Debugf("failed to export to %s", exportFilename)
log.Error().Err(err).Str("filename", exportFilename).Msg("failed to export")
} else {
logrus.Debugf("exported to %s", exportFilename)
log.Debug().Str("filename", exportFilename).Msg("file exported")
}
}

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

9
api/build/variables.go Normal file
View File

@@ -0,0 +1,9 @@
package build
// Variables to be set during the build time
var BuildNumber string
var ImageTag string
var NodejsVersion string
var YarnVersion string
var WebpackVersion string
var GoVersion string

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,17 @@ package chisel
import (
"context"
"fmt"
"github.com/portainer/portainer/api/http/proxy"
"log"
"net/http"
"strconv"
"sync"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
"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/rs/zerolog/log"
)
const (
@@ -28,18 +28,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,17 +59,17 @@ 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
func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
go func() {
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: start for %.0f minutes]\n", endpointID, maxAlive.Minutes())
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("max_alive_minutes", maxAlive.Minutes()).
Msg("start")
maxAliveTicker := time.NewTicker(maxAlive)
defer maxAliveTicker.Stop()
pingTicker := time.NewTicker(tunnelCleanupInterval)
@@ -80,14 +81,25 @@ func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx con
service.SetTunnelStatusToActive(endpointID)
err := service.pingAgent(endpointID)
if err != nil {
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [warning: ping agent err=%s]\n", endpointID, err)
log.Debug().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("ping agent")
}
case <-maxAliveTicker.C:
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: stop as %.0f minutes timeout]\n", endpointID, maxAlive.Minutes())
log.Debug().
Int("endpoint_id", int(endpointID)).
Float64("timeout_minutes", maxAlive.Minutes()).
Msg("tunnel keep alive timeout")
return
case <-ctx.Done():
err := ctx.Err()
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: stop as err=%s]\n", endpointID, err)
log.Debug().
Int("endpoint_id", int(endpointID)).
Err(err).
Msg("tunnel stop")
return
}
}
@@ -166,7 +178,10 @@ func (service *Service) retrievePrivateKeySeed() (string, error) {
}
func (service *Service) startTunnelVerificationLoop() {
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
log.Debug().
Float64("check_interval_seconds", tunnelCleanupInterval.Seconds()).
Msg("starting tunnel management process")
ticker := time.NewTicker(tunnelCleanupInterval)
for {
@@ -174,10 +189,12 @@ func (service *Service) startTunnelVerificationLoop() {
case <-ticker.C:
service.checkTunnels()
case <-service.shutdownCtx.Done():
log.Println("[DEBUG] Shutting down tunnel service")
log.Debug().Msg("shutting down tunnel service")
if err := service.StopTunnelServer(); err != nil {
log.Printf("Stopped tunnel service: %s", err)
log.Debug().Err(err).Msg("stopped tunnel service")
}
ticker.Stop()
return
}
@@ -185,44 +202,57 @@ 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.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
Float64("status_time_seconds", elapsed.Seconds()).
Msg("environment tunnel monitoring")
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
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.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
Float64("status_time_seconds", elapsed.Seconds()).
Float64("timeout_seconds", requiredTimeout.Seconds()).
Msg("REQUIRED state timeout exceeded")
}
if tunnel.Status == portainer.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.Debug().
Int("endpoint_id", int(endpointID)).
Str("status", tunnel.Status).
Float64("status_time_seconds", elapsed.Seconds()).
Float64("timeout_seconds", activeTimeout.Seconds()).
Msg("ACTIVE state timeout exceeded")
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.Error().
Int("endpoint_id", int(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)
err).
Msg("unable to snapshot Edge environment")
}
}
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

@@ -2,15 +2,14 @@ package cli
import (
"errors"
"log"
"os"
"path/filepath"
"strings"
"time"
portainer "github.com/portainer/portainer/api"
"os"
"path/filepath"
"strings"
"github.com/rs/zerolog/log"
"gopkg.in/alecthomas/kingpin.v2"
)
@@ -35,6 +34,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
DemoEnvironment: kingpin.Flag("demo", "Demo environment").Bool(),
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
@@ -50,13 +50,19 @@ 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(),
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("PRETTY", "JSON"),
}
kingpin.Parse()
@@ -96,11 +102,11 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
if *flags.NoAnalytics {
log.Println("Warning: The --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect.")
log.Warn().Msg("the --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect")
}
if *flags.SSL {
log.Println("Warning: SSL is enabled by default and there is no need for the --ssl flag. It has been kept to allow migration of instances running a previous version of Portainer with this flag enabled")
log.Warn().Msg("SSL is enabled by default and there is no need for the --ssl flag, it has been kept to allow migration of instances running a previous version of Portainer with this flag enabled")
}
}
@@ -125,7 +131,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

@@ -1,11 +1,10 @@
package main
import (
"log"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
func importFromJson(fileService portainer.FileService, store *datastore.Store) {
@@ -13,17 +12,17 @@ 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)
log.Error().Str("filename", importFile).Err(err).Msg("import failed")
// TODO: should really rollback on failure, but then we have nothing.
} else {
logrus.Printf("Successfully imported %s to new portainer database", importFile)
log.Info().Str("filename", importFile).Msg("successfully imported the file to a new portainer database")
}
// TODO: this is bad - its to ensure that any defaults that were broken in import, or migrations get set back to what we want
// I also suspect that everything from "Init to Init" is potentially a migration
err := store.Init()
if err != nil {
log.Fatalf("failed initializing data store: %v", err)
log.Fatal().Err(err).Msg("failed initializing data store")
}
}
}

View File

@@ -1,19 +1,55 @@
package main
import (
"log"
"fmt"
stdlog "log"
"os"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors"
)
func configureLogger() {
logger := logrus.New() // logger is to implicitly substitute stdlib's log
log.SetOutput(logger.Writer())
zerolog.ErrorStackFieldName = "stack_trace"
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
formatter := &logrus.TextFormatter{DisableTimestamp: true, DisableLevelTruncation: true}
logger.SetFormatter(formatter)
logrus.SetFormatter(formatter)
stdlog.SetFlags(0)
stdlog.SetOutput(log.Logger)
logger.SetLevel(logrus.DebugLevel)
logrus.SetLevel(logrus.DebugLevel)
log.Logger = log.Logger.With().Caller().Stack().Logger()
}
func setLoggingLevel(level string) {
switch level {
case "ERROR":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case "WARN":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "INFO":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "DEBUG":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
}
func setLoggingMode(mode string) {
switch mode {
case "PRETTY":
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
NoColor: true,
TimeFormat: "2006/01/02 03:04PM",
FormatMessage: formatMessage})
case "JSON":
log.Logger = log.Output(os.Stderr)
}
}
func formatMessage(i interface{}) string {
if i == nil {
return ""
}
return fmt.Sprintf("%s |", i)
}

View File

@@ -2,25 +2,26 @@ package main
import (
"context"
"crypto/sha256"
"fmt"
"log"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/portainer/libhelm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/build"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
"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/demo"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/exec"
"github.com/portainer/portainer/api/filesystem"
@@ -41,81 +42,97 @@ import (
"github.com/portainer/portainer/api/oauth"
"github.com/portainer/portainer/api/scheduler"
"github.com/portainer/portainer/api/stacks"
"github.com/rs/zerolog/log"
)
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)
log.Fatal().Err(err).Msg("failed parsing flags")
}
err = cliService.ValidateFlags(flags)
if err != nil {
log.Fatalf("failed validating flags:%v", err)
log.Fatal().Err(err).Msg("failed validating flags")
}
return flags
}
func initFileService(dataStorePath string) portainer.FileService {
fileService, err := filesystem.NewService(dataStorePath, "")
if err != nil {
log.Fatalf("failed creating file service: %v", err)
log.Fatal().Err(err).Msg("failed creating file service")
}
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)
log.Fatal().Err(err).Msg("failed creating database connection")
}
if bconn, ok := connection.(*boltdb.DbConnection); ok {
bconn.MaxBatchSize = *flags.MaxBatchSize
bconn.MaxBatchDelay = *flags.MaxBatchDelay
bconn.InitialMmapSize = *flags.InitialMmapSize
} else {
log.Fatal().Msg("failed creating database connection: expecting a boltdb database type but a different one was received")
}
store := datastore.NewStore(*flags.Data, fileService, connection)
isNew, err := store.Open()
if err != nil {
log.Fatalf("failed opening store: %v", err)
log.Fatal().Err(err).Msg("failed opening store")
}
if *flags.Rollback {
err := store.Rollback(false)
if err != nil {
log.Fatalf("failed rolling back: %s", err)
log.Fatal().Err(err).Msg("failed rolling back")
}
log.Println("Exiting rollback")
log.Info().Msg("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)
log.Fatal().Err(err).Msg("failed initializing data store")
}
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)
log.Fatal().Err(err).Msg("failed updating settings from flags")
}
} else {
storedVersion, err := store.VersionService.DBVersion()
if err != nil {
log.Fatal().Err(err).Msg("failure during creation of new database")
}
if storedVersion != portainer.DBVersion {
err = store.MigrateData()
if err != nil {
log.Fatal().Err(err).Msg("failed migration")
}
}
}
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.Fatal().Err(err).Msg("failed updating settings from flags")
}
// this is for the db restore functionality - needs more tests.
@@ -127,19 +144,19 @@ 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)
log.Error().Str("filename", exportFilename).Err(err).Msg("failed to export")
} else {
logrus.Debugf("exported to %s", exportFilename)
log.Debug().Str("filename", exportFilename).Msg("exported")
}
connection.Close()
}()
return store
}
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)
log.Fatal().Err(err).Msg("failed creating compose manager")
}
return composeWrapper
@@ -173,6 +190,7 @@ func initJWTService(userSessionTimeout string, dataStore dataservices.DataStore)
if err != nil {
return nil, err
}
return jwtService, nil
}
@@ -192,11 +210,11 @@ func initOAuthService() portainer.OAuthService {
return oauth.NewService()
}
func initGitService() portainer.GitService {
return git.NewService()
func initGitService(ctx context.Context) portainer.GitService {
return git.NewService(ctx)
}
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 == "" {
@@ -217,15 +235,15 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService,
return docker.NewClientFactory(signatureService, reverseTunnelService)
}
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *kubecli.ClientFactory {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore)
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) {
return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout)
}
func initSnapshotService(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 +264,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 +284,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 +302,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)
@@ -315,11 +345,7 @@ func enableFeaturesFromFlags(dataStore dataservices.DataStore, flags *portainer.
return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name)
}
if featureState {
log.Printf("Feature %v : on", *correspondingFeature)
} else {
log.Printf("Feature %v : off", *correspondingFeature)
}
log.Info().Str("feature", string(*correspondingFeature)).Bool("state", featureState).Msg("")
settings.FeatureFlagSettings[*correspondingFeature] = featureState
}
@@ -347,7 +373,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)
log.Fatal().Err(err).Msg("failed checking for existing key pair")
}
if existingKeyPair {
@@ -380,7 +406,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 +443,11 @@ 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)
log.Error().
Str("endpoint", endpoint.Name).
Str("URL", endpoint.URL).
Err(err).
Msg("environment snapshot error")
}
return dataStore.Endpoint().Create(endpoint)
@@ -442,7 +471,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 +492,10 @@ 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)
log.Error().
Str("endpoint", endpoint.Name).
Str("URL", endpoint.URL).Err(err).
Msg("environment snapshot error")
}
return dataStore.Endpoint().Create(endpoint)
@@ -481,7 +512,8 @@ 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.")
log.Info().Msg("instance already has defined environments, skipping the environment defined via CLI")
return nil
}
@@ -491,69 +523,94 @@ 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) {
log.Info().Str("filename", keyfilename).Msg("encryption key file not present")
} else {
log.Info().Err(err).Msg("error reading encryption key file")
}
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 {
log.Info().Msg("proceeding without encryption key")
}
dataStore := initDataStore(flags, fileService, shutdownCtx)
dataStore := initDataStore(flags, encryptionKey, fileService, shutdownCtx)
if err := dataStore.CheckCurrentEdition(); err != nil {
log.Fatal(err)
log.Fatal().Err(err).Msg("")
}
instanceID, err := dataStore.Version().InstanceID()
if err != nil {
log.Fatalf("failed getting instance id: %v", err)
log.Fatal().Err(err).Msg("failed getting instance id")
}
apiKeyService := initAPIKeyService(dataStore)
settings, err := dataStore.Settings().Settings()
if err != nil {
log.Fatal(err)
log.Fatal().Err(err).Msg("")
}
jwtService, err := initJWTService(settings.UserSessionTimeout, dataStore)
if err != nil {
log.Fatalf("failed initializing JWT service: %v", err)
log.Fatal().Err(err).Msg("failed initializing JWT service")
}
err = enableFeaturesFromFlags(dataStore, flags)
if err != nil {
log.Fatalf("failed enabling feature flag: %v", err)
log.Fatal().Err(err).Msg("failed enabling feature flag")
}
ldapService := initLDAPService()
oauthService := initOAuthService()
gitService := initGitService()
openAMTService := openamt.NewService(dataStore)
oauthService := initOAuthService()
gitService := initGitService(shutdownCtx)
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)
log.Fatal().Err(err).Msg("")
}
sslSettings, err := sslService.GetSSLSettings()
if err != nil {
log.Fatalf("failed to get ssl settings: %s", err)
log.Fatal().Err(err).Msg("failed to get SSL settings")
}
err = initKeyPair(fileService, digitalSignatureService)
if err != nil {
log.Fatalf("failed initializing key pair: %v", err)
log.Fatal().Err(err).Msg("failed initializing key pair")
}
reverseTunnelService := chisel.NewService(dataStore, shutdownCtx)
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore)
kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx)
if err != nil {
log.Fatalf("failed initializing snapshot service: %v", err)
log.Fatal().Err(err).Msg("failed initializing snapshot service")
}
snapshotService.Start()
@@ -562,9 +619,9 @@ 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)
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService)
reverseTunnelService.ProxyManager = proxyManager
@@ -574,37 +631,46 @@ 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)
log.Fatal().Err(err).Msg("failed initializing swarm stack manager")
}
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)
log.Fatal().Err(err).Msg("failed initializing helm package manager")
}
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil {
log.Fatalf("failed loading edge jobs from database: %v", err)
log.Fatal().Err(err).Msg("failed loading edge jobs from database")
}
applicationStatus := initStatus(instanceID)
demoService := demo.NewService()
if *flags.DemoEnvironment {
err := demoService.Init(dataStore, cryptoService)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing demo environment")
}
}
err = initEndpoint(flags, dataStore, snapshotService)
if err != nil {
log.Fatalf("failed initializing environment: %v", err)
log.Fatal().Err(err).Msg("failed initializing environment")
}
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
if err != nil {
log.Fatalf("failed getting admin password file: %v", err)
log.Fatal().Err(err).Msg("failed getting admin password file")
}
adminPasswordHash, err = cryptoService.Hash(strings.TrimSuffix(string(content), "\n"))
if err != nil {
log.Fatalf("failed hashing admin password: %v", err)
log.Fatal().Err(err).Msg("failed hashing admin password")
}
} else if *flags.AdminPassword != "" {
adminPasswordHash = *flags.AdminPassword
@@ -613,39 +679,57 @@ 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)
log.Fatal().Err(err).Msg("failed getting admin user")
}
if len(users) == 0 {
log.Println("Created admin user with the given password.")
log.Info().Msg("created admin user with the given password.")
user := &portainer.User{
Username: "admin",
Role: portainer.AdministratorRole,
Password: adminPasswordHash,
}
err := dataStore.User().Create(user)
if err != nil {
log.Fatalf("failed creating admin user: %v", err)
log.Fatal().Err(err).Msg("failed creating admin user")
}
} else {
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
log.Info().Msg("instance already has an administrator user defined, skipping admin password related flags.")
}
}
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotService)
if err != nil {
log.Fatalf("failed starting tunnel server: %s", err)
log.Fatal().Err(err).Msg("failed starting tunnel server")
}
sslDBSettings, err := dataStore.SSLSettings().Settings()
if err != nil {
log.Fatalf("failed to fetch ssl settings from DB")
log.Fatal().Msg("failed to fetch SSL settings from DB")
}
scheduler := scheduler.NewScheduler(shutdownCtx)
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
stacks.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
// FIXME: In 2.16 we changed the way ingress controller permissions are
// stored. Instead of being stored as annotation on an ingress rule, we keep
// them in our database. However, in order to run the migration we need an
// admin kube client to run lookup the old ingress rules and compare them
// with the current existing ingress classes.
//
// Unfortunately, our migrations run as part of the database initialization
// and our kubeclients require an initialized database. So it is not
// possible to do this migration as part of our normal flow. We DO have a
// migration which toggles a boolean in kubernetes configuration that
// indicated that this "post init" migration should be run. If/when this is
// resolved we can remove this function.
err = kubernetesClientFactory.PostInitMigrateIngresses()
if err != nil {
log.Fatal().Err(err).Msg("failure during creation of new database")
}
return &http.Server{
AuthorizationService: authorizationService,
ReverseTunnelService: reverseTunnelService,
@@ -669,7 +753,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,19 +763,32 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
BaseURL: *flags.BaseURL,
DemoService: demoService,
}
}
func main() {
configureLogger()
setLoggingMode("PRETTY")
flags := initCLI()
configureLogger()
setLoggingLevel(*flags.LogLevel)
setLoggingMode(*flags.LogMode)
for {
server := buildServer(flags)
log.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
log.Info().
Str("version", portainer.APIVersion).
Str("build_number", build.BuildNumber).
Str("image_tag", build.ImageTag).
Str("nodejs_version", build.NodejsVersion).
Str("yarn_version", build.YarnVersion).
Str("webpack_version", build.WebpackVersion).
Str("go_version", build.GoVersion).
Msg("starting Portainer")
err := server.Start()
log.Printf("[INFO] [cmd,main] Http server exited: %s\n", err)
log.Info().Err(err).Msg("HTTP server exited")
}
}

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(t, 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(t, 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

@@ -7,13 +7,11 @@ import (
"path/filepath"
"testing"
"github.com/docker/docker/pkg/ioutils"
"github.com/stretchr/testify/assert"
)
func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "encrypt")
defer os.RemoveAll(tmpdir)
tmpdir := t.TempDir()
var (
originFilePath = filepath.Join(tmpdir, "origin")
@@ -52,8 +50,7 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
}
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "encrypt")
defer os.RemoveAll(tmpdir)
tmpdir := t.TempDir()
var (
originFilePath = filepath.Join(tmpdir, "origin")
@@ -92,8 +89,7 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
}
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
tmpdir, _ := ioutils.TempDir("", "encrypt")
defer os.RemoveAll(tmpdir)
tmpdir := t.TempDir()
var (
originFilePath = filepath.Join(tmpdir, "origin")

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,130 @@ import (
"path"
"time"
"github.com/boltdb/bolt"
"github.com/portainer/portainer/api/dataservices/errors"
dserrors "github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
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)
// }
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
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 +157,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 +180,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 +195,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 +207,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 +242,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 +269,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 +284,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 +301,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 +312,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 +337,7 @@ func (connection *DbConnection) CreateObjectWithSetSequence(bucketName string, i
return err
}
data, err := MarshalObject(obj)
data, err := connection.MarshalObject(obj)
if err != nil {
return err
}
@@ -255,7 +352,7 @@ func (connection *DbConnection) GetAll(bucketName string, obj interface{}, appen
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
}
@@ -267,6 +364,7 @@ func (connection *DbConnection) GetAll(bucketName string, obj interface{}, appen
return nil
})
return err
}
@@ -277,7 +375,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 +389,44 @@ 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 {
log.Error().Str("bucket", bucketName).Msg("failed to restore metadata to bucket, skipped")
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"
"github.com/rs/zerolog/log"
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) {
log.Debug().Str("databasePath", databasePath).Msg("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,14 @@ func exportJson(databasePath string) ([]byte, error) {
defer connection.Close()
backup := make(map[string]interface{})
if metadata {
meta, err := backupMetadata(connection)
if err != nil {
log.Error().Err(err).Msg("failed exporting metadata")
}
backup["__metadata"] = meta
}
err = connection.View(func(tx *bolt.Tx) error {
err = tx.ForEach(func(name []byte, bucket *bolt.Bucket) error {
@@ -30,35 +60,51 @@ func exportJson(databasePath string) ([]byte, error) {
if v == nil {
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))
log.Error().
Str("bucket", bucketName).
Str("object", string(v)).
Err(err).
Msg("failed to unmarshal")
obj = v
}
if bucketName == "version" {
version[string(k)] = string(v)
} else {
list = append(list, obj)
}
}
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
})
return err
})
if err != nil {
return []byte("{}"), err
}

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":"","Credentials":{"MPSUser":"","MPSPassword":"","MPSToken":""},"DomainConfiguration":{"CertFileText":"","CertPassword":"","DomainName":""},"WirelessConfiguration":null},"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,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
passphrase = "my secret key"
)
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

@@ -0,0 +1,17 @@
package models
type (
K8sConfigMapOrSecret struct {
UID string `json:"UID"`
Name string `json:"Name"`
Namespace string `json:"Namespace"`
CreationDate string `json:"CreationDate"`
Annotations map[string]string `json:"Annotations"`
Data map[string]string `json:"Data"`
Applications []string `json:"Applications"`
IsSecret bool `json:"IsSecret"`
// SecretType will be an empty string for config maps.
SecretType string `json:"SecretType"`
}
)

View File

@@ -0,0 +1,75 @@
package models
import (
"errors"
"net/http"
)
type (
K8sIngressController struct {
Name string `json:"Name"`
ClassName string `json:"ClassName"`
Type string `json:"Type"`
Availability bool `json:"Availability"`
New bool `json:"New"`
Used bool `json:"Used"`
}
K8sIngressControllers []K8sIngressController
K8sIngressInfo struct {
Name string `json:"Name"`
UID string `json:"UID"`
Type string `json:"Type"`
Namespace string `json:"Namespace"`
ClassName string `json:"ClassName"`
Annotations map[string]string `json:"Annotations"`
Hosts []string `json:"Hosts"`
Paths []K8sIngressPath `json:"Paths"`
TLS []K8sIngressTLS `json:"TLS"`
}
K8sIngressTLS struct {
Hosts []string `json:"Hosts"`
SecretName string `json:"SecretName"`
}
K8sIngressPath struct {
IngressName string `json:"IngressName"`
Host string `json:"Host"`
ServiceName string `json:"ServiceName"`
Port int `json:"Port"`
Path string `json:"Path"`
PathType string `json:"PathType"`
}
// K8sIngressDeleteRequests is a mapping of namespace names to a slice of
// ingress names.
K8sIngressDeleteRequests map[string][]string
)
func (r K8sIngressControllers) Validate(request *http.Request) error {
return nil
}
func (r K8sIngressInfo) Validate(request *http.Request) error {
if r.Name == "" {
return errors.New("missing ingress name from the request payload")
}
if r.Namespace == "" {
return errors.New("missing ingress Namespace from the request payload")
}
return nil
}
func (r K8sIngressDeleteRequests) Validate(request *http.Request) error {
if len(r) == 0 {
return errors.New("missing deletion request list in payload")
}
for ns := range r {
if len(ns) == 0 {
return errors.New("deletion given with empty namespace")
}
}
return nil
}

View File

@@ -0,0 +1,12 @@
package models
import "net/http"
type K8sNamespaceDetails struct {
Name string `json:"Name"`
Annotations map[string]string `json:"Annotations"`
}
func (r *K8sNamespaceDetails) Validate(request *http.Request) error {
return nil
}

View File

@@ -0,0 +1,64 @@
package models
import (
"errors"
"net/http"
)
type (
K8sServiceInfo struct {
Name string `json:"Name"`
UID string `json:"UID"`
Type string `json:"Type"`
Namespace string `json:"Namespace"`
Annotations map[string]string `json:"Annotations"`
CreationTimestamp string `json:"CreationTimestamp"`
Labels map[string]string `json:"Labels"`
AllocateLoadBalancerNodePorts *bool `json:"AllocateLoadBalancerNodePorts,omitempty"`
Ports []K8sServicePort `json:"Ports"`
Selector map[string]string `json:"Selector"`
IngressStatus []K8sServiceIngress `json:"IngressStatus"`
}
K8sServicePort struct {
Name string `json:"Name"`
NodePort int `json:"NodePort"`
Port int `json:"Port"`
Protocol string `json:"Protocol"`
TargetPort int `json:"TargetPort"`
}
K8sServiceIngress struct {
IP string `json:"IP"`
Host string `json:"Host"`
}
// K8sServiceDeleteRequests is a mapping of namespace names to a slice of
// service names.
K8sServiceDeleteRequests map[string][]string
)
func (s *K8sServiceInfo) Validate(request *http.Request) error {
if s.Name == "" {
return errors.New("missing service name from the request payload")
}
if s.Namespace == "" {
return errors.New("missing service namespace from the request payload")
}
if s.Ports == nil {
return errors.New("missing service ports from the request payload")
}
return nil
}
func (r K8sServiceDeleteRequests) Validate(request *http.Request) error {
if len(r) == 0 {
return errors.New("missing deletion request list in payload")
}
for ns := range r {
if len(ns) == 0 {
return errors.New("deletion given with empty namespace")
}
}
return nil
}

View File

@@ -6,7 +6,8 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -41,12 +42,14 @@ func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer
func(obj interface{}) (interface{}, error) {
record, ok := obj.(*portainer.APIKey)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to APIKey object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
}
if record.UserID == userID {
result = append(result, *record)
}
return &portainer.APIKey{}, nil
})
@@ -64,18 +67,21 @@ func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, err
func(obj interface{}) (interface{}, error) {
key, ok := obj.(*portainer.APIKey)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to APIKey object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to APIKey object")
return nil, fmt.Errorf("Failed to convert to APIKey object: %s", obj)
}
if bytes.Equal(key.Digest, digest) {
k = key
return nil, stop
}
return &portainer.APIKey{}, nil
})
if err == stop {
return k, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -44,10 +45,11 @@ func (service *Service) CustomTemplates() ([]portainer.CustomTemplate, error) {
//var tag portainer.Tag
customTemplate, ok := obj.(*portainer.CustomTemplate)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to CustomTemplate object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to CustomTemplate object")
return nil, fmt.Errorf("Failed to convert to CustomTemplate object: %s", obj)
}
customTemplates = append(customTemplates, *customTemplate)
return &portainer.CustomTemplate{}, nil
})

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -43,10 +44,11 @@ func (service *Service) EdgeGroups() ([]portainer.EdgeGroup, error) {
func(obj interface{}) (interface{}, error) {
group, ok := obj.(*portainer.EdgeGroup)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to EdgeGroup object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeGroup object")
return nil, fmt.Errorf("Failed to convert to EdgeGroup object: %s", obj)
}
groups = append(groups, *group)
return &portainer.EdgeGroup{}, nil
})

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -41,13 +42,14 @@ func (service *Service) EdgeJobs() ([]portainer.EdgeJob, error) {
BucketName,
&portainer.EdgeJob{},
func(obj interface{}) (interface{}, error) {
//var tag portainer.Tag
job, ok := obj.(*portainer.EdgeJob)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to EdgeJob object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeJob object")
return nil, fmt.Errorf("Failed to convert to EdgeJob object: %s", obj)
}
edgeJobs = append(edgeJobs, *job)
return &portainer.EdgeJob{}, nil
})
@@ -67,14 +69,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

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -44,10 +45,12 @@ func (service *Service) EdgeStacks() ([]portainer.EdgeStack, error) {
//var tag portainer.Tag
stack, ok := obj.(*portainer.EdgeStack)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to EdgeStack object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EdgeStack object")
return nil, fmt.Errorf("Failed to convert to EdgeStack object: %s", obj)
}
stacks = append(stacks, *stack)
return &portainer.EdgeStack{}, nil
})
@@ -67,14 +70,15 @@ func (service *Service) EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStac
return &stack, nil
}
// CreateEdgeStack assign an ID to a new Edge stack and saves it.
func (service *Service) Create(edgeStack *portainer.EdgeStack) error {
return service.connection.CreateObject(
// CreateEdgeStack saves an Edge stack object to db.
func (service *Service) Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error {
edgeStack.ID = id
return service.connection.CreateObjectWithId(
BucketName,
func(id uint64) (int, interface{}) {
edgeStack.ID = portainer.EdgeStackID(id)
return int(edgeStack.ID), edgeStack
},
int(edgeStack.ID),
edgeStack,
)
}

View File

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

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -68,10 +69,12 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
func(obj interface{}) (interface{}, error) {
endpoint, ok := obj.(*portainer.Endpoint)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Endpoint object")
return nil, fmt.Errorf("Failed to convert to Endpoint object: %s", obj)
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Endpoint object")
return nil, fmt.Errorf("failed to convert to Endpoint object: %s", obj)
}
endpoints = append(endpoints, *endpoint)
return &portainer.Endpoint{}, nil
})

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -66,13 +67,14 @@ func (service *Service) EndpointGroups() ([]portainer.EndpointGroup, error) {
BucketName,
&portainer.EndpointGroup{},
func(obj interface{}) (interface{}, error) {
//var tag portainer.Tag
endpointGroup, ok := obj.(*portainer.EndpointGroup)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to EndpointGroup object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointGroup object")
return nil, fmt.Errorf("Failed to convert to EndpointGroup object: %s", obj)
}
endpointGroups = append(endpointGroups, *endpointGroup)
return &portainer.EndpointGroup{}, nil
})

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -33,7 +34,7 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
//EndpointRelations returns an array of all EndpointRelations
// EndpointRelations returns an array of all EndpointRelations
func (service *Service) EndpointRelations() ([]portainer.EndpointRelation, error) {
var all = make([]portainer.EndpointRelation, 0)
@@ -43,10 +44,12 @@ func (service *Service) EndpointRelations() ([]portainer.EndpointRelation, error
func(obj interface{}) (interface{}, error) {
r, ok := obj.(*portainer.EndpointRelation)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to EndpointRelation object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to EndpointRelation object")
return nil, fmt.Errorf("Failed to convert to EndpointRelation object: %s", obj)
}
all = append(all, *r)
return &portainer.EndpointRelation{}, nil
})

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

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -56,10 +57,12 @@ func (service *Service) Extensions() ([]portainer.Extension, error) {
func(obj interface{}) (interface{}, error) {
extension, ok := obj.(*portainer.Extension)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Extension object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Extension object")
return nil, fmt.Errorf("Failed to convert to Extension object: %s", obj)
}
extensions = append(extensions, *extension)
return &portainer.Extension{}, nil
})

View File

@@ -0,0 +1,95 @@
package fdoprofile
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "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 {
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("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

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -33,8 +34,8 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
//HelmUserRepository returns an array of all HelmUserRepository
func (service *Service) HelmUserRepositorys() ([]portainer.HelmUserRepository, error) {
// HelmUserRepository returns an array of all HelmUserRepository
func (service *Service) HelmUserRepositories() ([]portainer.HelmUserRepository, error) {
var repos = make([]portainer.HelmUserRepository, 0)
err := service.connection.GetAll(
@@ -43,10 +44,12 @@ func (service *Service) HelmUserRepositorys() ([]portainer.HelmUserRepository, e
func(obj interface{}) (interface{}, error) {
r, ok := obj.(*portainer.HelmUserRepository)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to HelmUserRepository object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to HelmUserRepository object")
return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
}
repos = append(repos, *r)
return &portainer.HelmUserRepository{}, nil
})
@@ -63,12 +66,14 @@ func (service *Service) HelmUserRepositoryByUserID(userID portainer.UserID) ([]p
func(obj interface{}) (interface{}, error) {
record, ok := obj.(*portainer.HelmUserRepository)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to HelmUserRepository object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to HelmUserRepository object")
return nil, fmt.Errorf("Failed to convert to HelmUserRepository object: %s", obj)
}
if record.UserID == userID {
result = append(result, *record)
}
return &portainer.HelmUserRepository{}, nil
})

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/edgetypes"
portainer "github.com/portainer/portainer/api"
)
@@ -28,15 +29,18 @@ type (
EdgeGroup() EdgeGroupService
EdgeJob() EdgeJobService
EdgeStack() EdgeStackService
EdgeUpdateSchedule() EdgeUpdateScheduleService
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
FDOProfile() FDOProfileService
HelmUserRepository() HelmUserRepositoryService
Registry() RegistryService
ResourceControl() ResourceControlService
Role() RoleService
APIKeyRepository() APIKeyRepository
Settings() SettingsService
Snapshot() SnapshotService
SSLSettings() SSLSettingsService
Stack() StackService
Tag() TagService
@@ -73,18 +77,29 @@ 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
BucketName() string
}
EdgeUpdateScheduleService interface {
ActiveSchedule(environmentID portainer.EndpointID) *edgetypes.EndpointUpdateScheduleRelation
ActiveSchedules(environmentIDs []portainer.EndpointID) []edgetypes.EndpointUpdateScheduleRelation
List() ([]edgetypes.UpdateSchedule, error)
Item(ID edgetypes.UpdateScheduleID) (*edgetypes.UpdateSchedule, error)
Create(edgeUpdateSchedule *edgetypes.UpdateSchedule) error
Update(ID edgetypes.UpdateScheduleID, edgeUpdateSchedule *edgetypes.UpdateSchedule) error
Delete(ID edgetypes.UpdateScheduleID) error
BucketName() string
}
// EdgeStackService represents a service to manage Edge stacks
EdgeStackService interface {
EdgeStacks() ([]portainer.EdgeStack, error)
EdgeStack(ID portainer.EdgeStackID) (*portainer.EdgeStack, error)
Create(edgeStack *portainer.EdgeStack) error
Create(id portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
UpdateEdgeStack(ID portainer.EdgeStackID, edgeStack *portainer.EdgeStack) error
DeleteEdgeStack(ID portainer.EdgeStackID) error
GetNextIdentifier() int
@@ -122,9 +137,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
@@ -189,6 +215,15 @@ type (
BucketName() string
}
SnapshotService interface {
Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error)
Snapshots() ([]portainer.Snapshot, error)
UpdateSnapshot(snapshot *portainer.Snapshot) error
DeleteSnapshot(endpointID portainer.EndpointID) error
Create(snapshot *portainer.Snapshot) error
BucketName() string
}
// SSLSettingsService represents a service for managing application settings
SSLSettingsService interface {
Settings() (*portainer.SSLSettings, error)

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -56,10 +57,12 @@ func (service *Service) Registries() ([]portainer.Registry, error) {
func(obj interface{}) (interface{}, error) {
registry, ok := obj.(*portainer.Registry)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Registry object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Registry object")
return nil, fmt.Errorf("Failed to convert to Registry object: %s", obj)
}
registries = append(registries, *registry)
return &portainer.Registry{}, nil
})

View File

@@ -4,8 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -59,7 +59,7 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
func(obj interface{}) (interface{}, error) {
rc, ok := obj.(*portainer.ResourceControl)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to ResourceControl object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
}
@@ -74,14 +74,12 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
return nil, stop
}
}
return &portainer.ResourceControl{}, nil
})
if err == stop {
return resourceControl, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
return nil, err
}
@@ -96,10 +94,12 @@ func (service *Service) ResourceControls() ([]portainer.ResourceControl, error)
func(obj interface{}) (interface{}, error) {
rc, ok := obj.(*portainer.ResourceControl)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to ResourceControl object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to ResourceControl object")
return nil, fmt.Errorf("Failed to convert to ResourceControl object: %s", obj)
}
rcs = append(rcs, *rc)
return &portainer.ResourceControl{}, nil
})

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -56,10 +57,12 @@ func (service *Service) Roles() ([]portainer.Role, error) {
func(obj interface{}) (interface{}, error) {
set, ok := obj.(*portainer.Role)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Role object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Role object")
return nil, fmt.Errorf("Failed to convert to Role object: %s", obj)
}
sets = append(sets, *set)
return &portainer.Role{}, nil
})

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -68,10 +69,12 @@ func (service *Service) Schedules() ([]portainer.Schedule, error) {
func(obj interface{}) (interface{}, error) {
schedule, ok := obj.(*portainer.Schedule)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Schedule object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Schedule object")
return nil, fmt.Errorf("Failed to convert to Schedule object: %s", obj)
}
schedules = append(schedules, *schedule)
return &portainer.Schedule{}, nil
})
@@ -89,12 +92,14 @@ func (service *Service) SchedulesByJobType(jobType portainer.JobType) ([]portain
func(obj interface{}) (interface{}, error) {
schedule, ok := obj.(*portainer.Schedule)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Schedule object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Schedule object")
return nil, fmt.Errorf("Failed to convert to Schedule object: %s", obj)
}
if schedule.JobType == jobType {
schedules = append(schedules, *schedule)
}
return &portainer.Schedule{}, nil
})

View File

@@ -0,0 +1,76 @@
package snapshot
import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
const (
BucketName = "snapshots"
)
type Service struct {
connection portainer.Connection
}
func (service *Service) BucketName() string {
return BucketName
}
func NewService(connection portainer.Connection) (*Service, error) {
err := connection.SetServiceName(BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
func (service *Service) Snapshot(endpointID portainer.EndpointID) (*portainer.Snapshot, error) {
var snapshot portainer.Snapshot
identifier := service.connection.ConvertToKey(int(endpointID))
err := service.connection.GetObject(BucketName, identifier, &snapshot)
if err != nil {
return nil, err
}
return &snapshot, nil
}
func (service *Service) Snapshots() ([]portainer.Snapshot, error) {
var snapshots = make([]portainer.Snapshot, 0)
err := service.connection.GetAllWithJsoniter(
BucketName,
&portainer.Snapshot{},
func(obj interface{}) (interface{}, error) {
snapshot, ok := obj.(*portainer.Snapshot)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Snapshot object")
return nil, fmt.Errorf("failed to convert to Snapshot object: %s", obj)
}
snapshots = append(snapshots, *snapshot)
return &portainer.Snapshot{}, nil
})
return snapshots, err
}
func (service *Service) UpdateSnapshot(snapshot *portainer.Snapshot) error {
identifier := service.connection.ConvertToKey(int(snapshot.EndpointID))
return service.connection.UpdateObject(BucketName, identifier, snapshot)
}
func (service *Service) DeleteSnapshot(endpointID portainer.EndpointID) error {
identifier := service.connection.ConvertToKey(int(endpointID))
return service.connection.DeleteObject(BucketName, identifier)
}
func (service *Service) Create(snapshot *portainer.Snapshot) error {
return service.connection.CreateObjectWithId(BucketName, int(snapshot.EndpointID), snapshot)
}

View File

@@ -4,10 +4,10 @@ import (
"fmt"
"strings"
"github.com/sirupsen/logrus"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
const (
@@ -60,13 +60,15 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.Stack)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
}
if stack.Name == name {
s = stack
return nil, stop
}
return &portainer.Stack{}, nil
})
if err == stop {
@@ -89,12 +91,14 @@ func (service *Service) StacksByName(name string) ([]portainer.Stack, error) {
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(portainer.Stack)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
}
if stack.Name == name {
stacks = append(stacks, stack)
}
return &portainer.Stack{}, nil
})
@@ -111,10 +115,12 @@ func (service *Service) Stacks() ([]portainer.Stack, error) {
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.Stack)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
}
stacks = append(stacks, *stack)
return &portainer.Stack{}, nil
})
@@ -156,13 +162,15 @@ func (service *Service) StackByWebhookID(id string) (*portainer.Stack, error) {
s, ok = obj.(*portainer.Stack)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return &portainer.Stack{}, nil
}
if s.AutoUpdate != nil && strings.EqualFold(s.AutoUpdate.Webhook, id) {
return nil, stop
}
return &portainer.Stack{}, nil
})
if err == stop {
@@ -186,12 +194,14 @@ func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
func(obj interface{}) (interface{}, error) {
stack, ok := obj.(*portainer.Stack)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Stack object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Stack object")
return nil, fmt.Errorf("Failed to convert to Stack object: %s", obj)
}
if stack.AutoUpdate != nil && stack.AutoUpdate.Interval != "" {
stacks = append(stacks, *stack)
}
return &portainer.Stack{}, nil
})

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(t, 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(t, true, true)
defer teardown()
staticStack := portainer.Stack{ID: 1}

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -43,10 +44,12 @@ func (service *Service) Tags() ([]portainer.Tag, error) {
func(obj interface{}) (interface{}, error) {
tag, ok := obj.(*portainer.Tag)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Tag object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Tag object")
return nil, fmt.Errorf("Failed to convert to Tag object: %s", obj)
}
tags = append(tags, *tag)
return &portainer.Tag{}, nil
})

View File

@@ -4,10 +4,10 @@ import (
"fmt"
"strings"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
const (
@@ -60,13 +60,15 @@ func (service *Service) TeamByName(name string) (*portainer.Team, error) {
func(obj interface{}) (interface{}, error) {
team, ok := obj.(*portainer.Team)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Team object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Team object")
return nil, fmt.Errorf("Failed to convert to Team object: %s", obj)
}
if strings.EqualFold(t.Name, name) {
if strings.EqualFold(team.Name, name) {
t = team
return nil, stop
}
return &portainer.Team{}, nil
})
if err == stop {
@@ -89,10 +91,12 @@ func (service *Service) Teams() ([]portainer.Team, error) {
func(obj interface{}) (interface{}, error) {
team, ok := obj.(*portainer.Team)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Team object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Team object")
return nil, fmt.Errorf("Failed to convert to Team object: %s", obj)
}
teams = append(teams, *team)
return &portainer.Team{}, nil
})

View File

@@ -0,0 +1,53 @@
package tests
import (
"testing"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
)
func Test_teamByName(t *testing.T) {
t.Run("When store is empty should return ErrObjectNotFound", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(t, true, true)
defer teardown()
_, err := store.Team().TeamByName("name")
assert.ErrorIs(t, err, errors.ErrObjectNotFound)
})
t.Run("When there is no object with the same name should return ErrObjectNotFound", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(t, true, true)
defer teardown()
teamBuilder := teamBuilder{
t: t,
store: store,
count: 0,
}
teamBuilder.createNew("name1")
_, err := store.Team().TeamByName("name")
assert.ErrorIs(t, err, errors.ErrObjectNotFound)
})
t.Run("When there is an object with the same name should return the object", func(t *testing.T) {
_, store, teardown := datastore.MustNewTestStore(t, true, true)
defer teardown()
teamBuilder := teamBuilder{
t: t,
store: store,
count: 0,
}
expectedTeam := teamBuilder.createNew("name1")
team, err := store.Team().TeamByName("name1")
assert.NoError(t, err, "TeamByName should succeed")
assert.Equal(t, expectedTeam, team)
})
}

View File

@@ -0,0 +1,28 @@
package tests
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
)
type teamBuilder struct {
t *testing.T
count int
store *datastore.Store
}
func (b *teamBuilder) createNew(name string) *portainer.Team {
b.count++
team := &portainer.Team{
ID: portainer.TeamID(b.count),
Name: name,
}
err := b.store.Team().Create(team)
assert.NoError(b.t, err)
return team
}

View File

@@ -4,7 +4,8 @@ import (
"fmt"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -56,10 +57,12 @@ func (service *Service) TeamMemberships() ([]portainer.TeamMembership, error) {
func(obj interface{}) (interface{}, error) {
membership, ok := obj.(*portainer.TeamMembership)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to TeamMembership object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
return nil, fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
}
memberships = append(memberships, *membership)
return &portainer.TeamMembership{}, nil
})
@@ -76,12 +79,14 @@ func (service *Service) TeamMembershipsByUserID(userID portainer.UserID) ([]port
func(obj interface{}) (interface{}, error) {
membership, ok := obj.(*portainer.TeamMembership)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to TeamMembership object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
return nil, fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
}
if membership.UserID == userID {
memberships = append(memberships, *membership)
}
return &portainer.TeamMembership{}, nil
})
@@ -98,12 +103,14 @@ func (service *Service) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]port
func(obj interface{}) (interface{}, error) {
membership, ok := obj.(*portainer.TeamMembership)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to TeamMembership object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
return nil, fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
}
if membership.TeamID == teamID {
memberships = append(memberships, *membership)
}
return &portainer.TeamMembership{}, nil
})
@@ -140,13 +147,15 @@ func (service *Service) DeleteTeamMembershipByUserID(userID portainer.UserID) er
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to TeamMembership object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
return -1, false
}
if membership.UserID == userID {
return int(membership.ID), true
}
return -1, false
})
}
@@ -158,13 +167,15 @@ func (service *Service) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) er
func(obj interface{}) (id int, ok bool) {
membership, ok := obj.(portainer.TeamMembership)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to TeamMembership object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to TeamMembership object")
//return fmt.Errorf("Failed to convert to TeamMembership object: %s", obj)
return -1, false
}
if membership.TeamID == teamID {
return int(membership.ID), true
}
return -1, false
})
}

View File

@@ -4,10 +4,10 @@ import (
"fmt"
"strings"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/rs/zerolog/log"
)
const (
@@ -59,18 +59,23 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
func(obj interface{}) (interface{}, error) {
user, ok := obj.(*portainer.User)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to User object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to User object")
return nil, fmt.Errorf("Failed to convert to User object: %s", obj)
}
if strings.EqualFold(user.Username, username) {
u = user
return nil, stop
}
return &portainer.User{}, nil
})
if err == stop {
return u, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
@@ -88,10 +93,13 @@ func (service *Service) Users() ([]portainer.User, error) {
func(obj interface{}) (interface{}, error) {
user, ok := obj.(*portainer.User)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to User object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to User object")
return nil, fmt.Errorf("Failed to convert to User object: %s", obj)
}
users = append(users, *user)
return &portainer.User{}, nil
})
@@ -108,12 +116,15 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
func(obj interface{}) (interface{}, error) {
user, ok := obj.(*portainer.User)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to User object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to User object")
return nil, fmt.Errorf("Failed to convert to User object: %s", obj)
}
if user.Role == role {
users = append(users, *user)
}
return &portainer.User{}, nil
})

View File

@@ -5,7 +5,8 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/sirupsen/logrus"
"github.com/rs/zerolog/log"
)
const (
@@ -34,7 +35,7 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
//Webhooks returns an array of all webhooks
// Webhooks returns an array of all webhooks
func (service *Service) Webhooks() ([]portainer.Webhook, error) {
var webhooks = make([]portainer.Webhook, 0)
@@ -44,10 +45,12 @@ func (service *Service) Webhooks() ([]portainer.Webhook, error) {
func(obj interface{}) (interface{}, error) {
webhook, ok := obj.(*portainer.Webhook)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Webhook object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Webhook object")
return nil, fmt.Errorf("Failed to convert to Webhook object: %s", obj)
}
webhooks = append(webhooks, *webhook)
return &portainer.Webhook{}, nil
})
@@ -77,18 +80,23 @@ func (service *Service) WebhookByResourceID(ID string) (*portainer.Webhook, erro
func(obj interface{}) (interface{}, error) {
webhook, ok := obj.(*portainer.Webhook)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Webhook object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Webhook object")
return nil, fmt.Errorf("Failed to convert to Webhook object: %s", obj)
}
if webhook.ResourceID == ID {
w = webhook
return nil, stop
}
return &portainer.Webhook{}, nil
})
if err == stop {
return w, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}
@@ -106,18 +114,23 @@ func (service *Service) WebhookByToken(token string) (*portainer.Webhook, error)
func(obj interface{}) (interface{}, error) {
webhook, ok := obj.(*portainer.Webhook)
if !ok {
logrus.WithField("obj", obj).Errorf("Failed to convert to Webhook object")
log.Debug().Str("obj", fmt.Sprintf("%#v", obj)).Msg("failed to convert to Webhook object")
return nil, fmt.Errorf("Failed to convert to Webhook object: %s", obj)
}
if webhook.Token == token {
w = webhook
return nil, stop
}
return &portainer.Webhook{}, nil
})
if err == stop {
return w, nil
}
if err == nil {
return nil, errors.ErrObjectNotFound
}

View File

@@ -6,7 +6,7 @@ import (
"path"
"time"
plog "github.com/portainer/portainer/api/datastore/log"
"github.com/rs/zerolog/log"
)
var backupDefaults = struct {
@@ -17,8 +17,6 @@ var backupDefaults = struct {
"common",
}
var backupLog = plog.NewScopedLog("database, backup")
//
// Backup Helpers
//
@@ -29,13 +27,13 @@ func (store *Store) createBackupFolders() {
commonDir := store.commonBackupDir()
if exists, _ := store.fileService.FileExists(commonDir); !exists {
if err := os.MkdirAll(commonDir, 0700); err != nil {
backupLog.Error("Error while creating common backup folder", err)
log.Error().Err(err).Msg("error while creating common backup folder")
}
}
}
func (store *Store) databasePath() string {
return path.Join(store.connection.GetStorePath(), store.connection.GetDatabaseFilename())
return store.connection.GetDatabaseFilePath()
}
func (store *Store) commonBackupDir() string {
@@ -43,11 +41,13 @@ func (store *Store) commonBackupDir() string {
}
func (store *Store) copyDBFile(from string, to string) error {
backupLog.Info(fmt.Sprintf("Copying db file from %s to %s", from, to))
log.Info().Str("from", from).Str("to", to).Msg("copying DB file")
err := store.fileService.Copy(from, to, true)
if err != nil {
backupLog.Error("Failed", err)
log.Error().Err(err).Msg("failed")
}
return err
}
@@ -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)
@@ -94,12 +99,32 @@ func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
// BackupWithOptions backup current database with options
func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
backupLog.Info("creating db backup")
log.Info().Msg("creating DB backup")
store.createBackupFolders()
options = store.setupOptions(options)
dbPath := store.databasePath()
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
if err := store.Close(); err != nil {
return options.BackupPath, fmt.Errorf(
"error closing datastore before creating backup: %v",
err,
)
}
if err := store.copyDBFile(dbPath, options.BackupPath); err != nil {
return options.BackupPath, err
}
if _, err := store.Open(); err != nil {
return options.BackupPath, fmt.Errorf(
"error opening datastore after creating backup: %v",
err,
)
}
return options.BackupPath, nil
}
// RestoreWithOptions previously saved backup for the current Edition with options
@@ -112,17 +137,19 @@ func (store *Store) restoreWithOptions(options *BackupOptions) error {
// Check if backup file exist before restoring
_, err := os.Stat(options.BackupPath)
if os.IsNotExist(err) {
backupLog.Error(fmt.Sprintf("Backup file to restore does not exist %s", options.BackupPath), err)
log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to restore does not exist %s")
return err
}
err = store.Close()
if err != nil {
backupLog.Error("Error while closing store before restore", err)
log.Error().Err(err).Msg("error while closing store before restore")
return err
}
backupLog.Info("Restoring db backup")
log.Info().Msg("restoring DB backup")
err = store.copyDBFile(options.BackupPath, store.databasePath())
if err != nil {
return err
@@ -134,20 +161,22 @@ func (store *Store) restoreWithOptions(options *BackupOptions) error {
// RemoveWithOptions removes backup database based on supplied options
func (store *Store) removeWithOptions(options *BackupOptions) error {
backupLog.Info("Removing db backup")
log.Info().Msg("removing DB backup")
options = store.setupOptions(options)
_, err := os.Stat(options.BackupPath)
if os.IsNotExist(err) {
backupLog.Error(fmt.Sprintf("Backup file to remove does not exist %s", options.BackupPath), err)
log.Error().Str("path", options.BackupPath).Err(err).Msg("backup file to remove does not exist")
return err
}
backupLog.Info(fmt.Sprintf("Removing db file at %s", options.BackupPath))
log.Info().Str("path", options.BackupPath).Msg("removing DB file")
err = os.Remove(options.BackupPath)
if err != nil {
backupLog.Error("Failed", err)
log.Error().Err(err).Msg("failed")
return err
}

View File

@@ -10,7 +10,7 @@ import (
)
func TestCreateBackupFolders(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(t, 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(t, 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(t, 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(t, 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,16 @@
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/rs/zerolog/log"
)
func (store *Store) version() (int, error) {
@@ -34,6 +40,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 +64,19 @@ 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 {
log.Debug().Int("version", version).Msg("opened existing store")
return false, nil
}
return newStore, nil
@@ -65,16 +95,84 @@ 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()
log.Info().Msg("encrypting database")
// export file path for backup
exportFilename := path.Join(store.databasePath() + "." + fmt.Sprintf("backup-%d.json", time.Now().Unix()))
log.Info().Str("filename", exportFilename).Msg("exporting database backup")
err = store.Export(exportFilename)
if err != nil {
log.Error().Str("filename", exportFilename).Err(err).Msg("failed to export")
return err
}
log.Info().Msg("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())
log.Fatal().Err(portainerErrors.ErrDBImportFailed).Msg("")
}
err = os.Remove(oldFilename)
if err != nil {
log.Error().Msg("failed to remove the un-encrypted db file")
}
err = os.Remove(exportFilename)
if err != nil {
log.Error().Msg("failed to remove the json backup file")
}
// Close db connection
store.connection.Close()
log.Info().Msg("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(t, 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{},

View File

@@ -7,41 +7,64 @@ 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: false,
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
InternalAuthSettings: portainer.InternalAuthSettings{
RequiredPasswordLength: 12,
},
LDAPSettings: portainer.LDAPSettings{
AnonymousMode: true,
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 +73,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 +120,5 @@ func (store *Store) Init() error {
return err
}
}
return nil
}

View File

@@ -1,41 +0,0 @@
package log
import (
"fmt"
"log"
)
const (
INFO = "INFO"
ERROR = "ERROR"
DEBUG = "DEBUG"
FATAL = "FATAL"
)
type ScopedLog struct {
scope string
}
func NewScopedLog(scope string) *ScopedLog {
return &ScopedLog{scope: scope}
}
func (slog *ScopedLog) print(kind string, message string) {
log.Printf("[%s] [%s] %s", kind, slog.scope, message)
}
func (slog *ScopedLog) Debug(message string) {
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) Error(message string, err error) {
slog.print(ERROR, fmt.Sprintf("[message: %s] [error: %s]", message, err))
}
func (slog *ScopedLog) NotImplemented(method string) {
log.Fatalf("[%s] [%s] [%s]", FATAL, slog.scope, fmt.Sprintf("%s is not yet implemented", method))
}

View File

@@ -4,37 +4,43 @@ import (
"fmt"
"runtime/debug"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/dataservices/errors"
plog "github.com/portainer/portainer/api/datastore/log"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/internal/authorization"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
var migrateLog = plog.NewScopedLog("database, migrate")
func (store *Store) MigrateData() error {
version, err := store.version()
if err != nil {
return err
}
// Backup Database
backupPath, err := store.Backup()
if err != nil {
return werrors.Wrap(err, "while backing up db before migration")
}
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,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
SnapshotService: store.SnapshotService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
@@ -45,7 +51,26 @@ func (store *Store) MigrateData() error {
AuthorizationService: authorization.NewService(store),
}
return store.connectionMigrateData(migratorParams)
// restore on error
err = store.connectionMigrateData(migratorParams)
if err != nil {
log.Error().Err(err).Msg("while DB migration, restoring DB")
// Restore options
options := BackupOptions{
BackupPath: backupPath,
}
err := store.restoreWithOptions(&options)
if err != nil {
log.Fatal().
Str("database_file", store.databasePath()).
Str("backup", options.BackupPath).Err(err).
Msg("failed restoring the backup, Portainer database file needs to restored manually by replacing the database file with a recent backup")
}
}
return err
}
// FailSafeMigrate backup and restore DB if migration fail
@@ -83,10 +108,15 @@ func (store *Store) connectionMigrateData(migratorParams *migrator.MigratorParam
}
if migrator.Version() < portainer.DBVersion {
migrateLog.Info(fmt.Sprintf("Migrating database from version %v to %v.\n", migrator.Version(), portainer.DBVersion))
log.Info().
Int("migrator_version", migrator.Version()).
Int("db_version", portainer.DBVersion).
Msg("migrating database")
err = store.FailSafeMigrate(migrator)
if err != nil {
migrateLog.Error("An error occurred during database migration", err)
log.Error().Err(err).Msg("an error occurred during database migration")
return err
}
}
@@ -96,17 +126,19 @@ func (store *Store) connectionMigrateData(migratorParams *migrator.MigratorParam
// backupVersion will backup the database or panic if any errors occur
func (store *Store) backupVersion(migrator *migrator.Migrator) error {
migrateLog.Info("Backing up database prior to version upgrade...")
log.Info().Msg("backing up database prior to version upgrade")
options := getBackupRestoreOptions(store.commonBackupDir())
_, err := store.backupWithOptions(options)
if err != nil {
migrateLog.Error("An error occurred during database backup", err)
log.Error().Err(err).Msg("an error occurred during database backup")
removalErr := store.removeWithOptions(options)
if removalErr != nil {
migrateLog.Error("An error occurred during store removal prior to backup", err)
log.Error().Err(err).Msg("an error occurred during store removal prior to backup")
}
return err
}

View File

@@ -1,13 +1,20 @@
package datastore
import (
"bytes"
"encoding/json"
"fmt"
"log"
"io"
"os"
"path/filepath"
"strings"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/google/go-cmp/cmp"
"github.com/rs/zerolog/log"
)
// testVersion is a helper which tests current store version against wanted version
@@ -22,8 +29,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 latest",
srcPath: "test_data/input_24.json",
wantPath: "test_data/output_24_to_latest.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(t, false, true)
defer teardown()
if !newStore {
@@ -50,7 +81,7 @@ func TestMigrateData(t *testing.T) {
{version: 21, expectedVersion: portainer.DBVersion},
}
for _, tc := range tests {
_, store, teardown := MustNewTestStore(true)
_, store, teardown := MustNewTestStore(t, true, true)
defer teardown()
// Setup data
@@ -75,7 +106,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(t, false, true)
defer teardown()
version := 17
@@ -87,7 +118,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(t, false, true)
defer teardown()
store.VersionService.StoreDBVersion(0)
@@ -101,7 +132,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(t, false, true)
defer teardown()
store.VersionService.StoreIsUpdating(true)
@@ -116,7 +147,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(t, false, true)
defer teardown()
store.MigrateData()
@@ -127,48 +158,48 @@ func TestMigrateData(t *testing.T) {
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
}
})
}
func Test_getBackupRestoreOptions(t *testing.T) {
_, store, teardown := MustNewTestStore(false)
_, store, teardown := MustNewTestStore(t, false, true)
defer teardown()
options := getBackupRestoreOptions(store.commonBackupDir())
wantDir := store.commonBackupDir()
if !strings.HasSuffix(options.BackupDir, wantDir) {
log.Fatalf("incorrect backup dir; got=%s, want=%s", options.BackupDir, wantDir)
log.Fatal().Str("got", options.BackupDir).Str("want", wantDir).Msg("incorrect backup dir")
}
wantFilename := "portainer.db.bak"
if options.BackupFileName != wantFilename {
log.Fatalf("incorrect backup file; got=%s, want=%s", options.BackupFileName, wantFilename)
log.Fatal().Str("got", options.BackupFileName).Str("want", wantFilename).Msg("incorrect backup file")
}
}
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(t, false, true)
defer teardown()
store.VersionService.StoreDBVersion(version)
_, err := store.backupWithOptions(getBackupRestoreOptions(store.commonBackupDir()))
if err != nil {
log.Fatal(err)
log.Fatal().Err(err).Msg("")
}
// Change the current edition
err = store.VersionService.StoreDBVersion(version + 10)
if err != nil {
log.Fatal(err)
log.Fatal().Err(err).Msg("")
}
err = store.Rollback(true)
if err != nil {
t.Logf("Rollback failed: %s", err)
t.Fail()
return
}
@@ -185,3 +216,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(t, 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(t, 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(t, false, true)
defer teardown()
stackService := store.Stack()

View File

@@ -1,16 +1,40 @@
package migrator
import (
"fmt"
"errors"
"reflect"
"runtime"
portainer "github.com/portainer/portainer/api"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
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 +43,104 @@ 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 2.14
newMigration(50, m.migrateDBVersionToDB50),
// Portainer 2.15
newMigration(60, m.migrateDBVersionToDB60),
// Portainer 2.16
newMigration(70, m.migrateDBVersionToDB70),
// Portainer 2.16.1
newMigration(71, m.migrateDBVersionToDB71),
}
// 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 {
log.Info().Int("to_version", migration.dbversion).Msg("migrating DB")
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointGroupsToDBVersion18")
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return migrationError(err, "updateRegistriesToDBVersion18")
err := migration.migrate()
if err != nil {
return migrationError(err, GetFunctionName(migration.migrate))
}
}
lastDbVersion = migration.dbversion
}
// 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")
}
}
log.Info().Int("version", portainer.DBVersion).Msg("setting DB version")
err = m.versionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return migrationError(err, "StoreDBVersion")
}
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
log.Info().Int("version", portainer.DBVersion).Msg("updated DB version")
// reset DB updating status
return m.versionService.StoreIsUpdating(false)

View File

@@ -2,9 +2,13 @@ package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
func (m *Migrator) updateUsersToDBVersion18() error {
log.Info().Msg("updating users")
legacyUsers, err := m.userService.Users()
if err != nil {
return err
@@ -39,6 +43,8 @@ func (m *Migrator) updateUsersToDBVersion18() error {
}
func (m *Migrator) updateEndpointsToDBVersion18() error {
log.Info().Msg("updating endpoints")
legacyEndpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
@@ -69,6 +75,8 @@ func (m *Migrator) updateEndpointsToDBVersion18() error {
}
func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
log.Info().Msg("updating endpoint groups")
legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups()
if err != nil {
return err
@@ -99,6 +107,8 @@ func (m *Migrator) updateEndpointGroupsToDBVersion18() error {
}
func (m *Migrator) updateRegistriesToDBVersion18() error {
log.Info().Msg("updating registries")
legacyRegistries, err := m.registryService.Registries()
if err != nil {
return err

View File

@@ -1,8 +1,14 @@
package migrator
import portainer "github.com/portainer/portainer/api"
import (
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
)
func (m *Migrator) updateSettingsToDBVersion19() error {
log.Info().Msg("updating settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err

View File

@@ -2,11 +2,15 @@ package migrator
import (
"strings"
"github.com/rs/zerolog/log"
)
const scheduleScriptExecutionJobType = 1
func (m *Migrator) updateUsersToDBVersion20() error {
log.Info().Msg("updating user authentication")
return m.authorizationService.UpdateUsersAuthorizations()
}
@@ -22,6 +26,8 @@ func (m *Migrator) updateSettingsToDBVersion20() error {
}
func (m *Migrator) updateSchedulesToDBVersion20() error {
log.Info().Msg("updating schedules")
legacySchedules, err := m.scheduleService.Schedules()
if err != nil {
return err

View File

@@ -3,9 +3,13 @@ package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/rs/zerolog/log"
)
func (m *Migrator) updateResourceControlsToDBVersion22() error {
log.Info().Msg("updating resource controls")
legacyResourceControls, err := m.resourceControlService.ResourceControls()
if err != nil {
return err
@@ -24,6 +28,8 @@ func (m *Migrator) updateResourceControlsToDBVersion22() error {
}
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
log.Info().Msg("updating users and roles")
legacyUsers, err := m.userService.Users()
if err != nil {
return err

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