Compare commits

...

297 Commits

Author SHA1 Message Date
congs 4cc3f6bab4 fix(stack): EE-4213 Allow latest image to be pulled for stacks: backport backend logic (#7670)
/ triage (push) Has been cancelled
Lint / Run linters (push) Has been cancelled
Test / test-client (push) Has been cancelled
2022-09-15 16:57:36 +12:00
congs 6330cc885c fix(stack): EE-4213 Allow latest image to be pulled for stacks (#7654) 2022-09-14 10:17:27 +12:00
itsconquest cd3381ee59 fix(theme): tabs and codeeditor darkmode correction [EE-4188] (#7642)
* fix(theme): tabs and codeeditor darkmode correction [EE-4188]

* correct codemirror background
2022-09-12 17:07:07 +12:00
itsconquest 7b7234c79a fix(theme): env sidebar darkmode color [EE-4188] (#7637)
* fix(theme): env sidebar darkmode color [EE-4188]

* style usericon

* further dark mode changes
2022-09-09 12:47:10 +12:00
Prabhat Khera a965f5d538 bump version to 2.15.1 (#7634) 2022-09-08 16:55:10 +12:00
itsconquest b31176da7a fix(auth): prevent trim on password [EE-4197] (#7632) 2022-09-08 13:50:17 +12:00
LP B aacf4eb880 fix(theme): update dark mode colors [EE-4188] (#7628)
* fix(theme): update dark mode colors [EE-4188]

* fix sidebar hover/selected

Co-authored-by: itsconquest <william.conquest@portainer.io>
2022-09-08 13:49:14 +12:00
LP B b19e9906fc fix(images/build): enforce file content length only when using the editor (#7629) 2022-09-08 02:32:41 +02:00
Dmitry Salakhov 576456efb2 fix: don't url-escape socket paths (#7622) 2022-09-08 11:45:01 +12:00
itsconquest 103fbbfe6e fix(boxselector): fix darkmode BE teaser style [EE-4145] (#7597)
Test / test-client (push) Has been cancelled
* 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:43 +12:00
Oscar Zhou c8044cc125 fix(stack/compose): remove the orphan containers if stack deployment is failed (#7600) 2022-09-02 08:11:14 +12:00
LP B 154f2ed189 fix(container/edit): fallback value when retrieving GPU config without snapshot available [EE-4110] (#7571) 2022-08-30 14:52:32 +02:00
itsconquest 1a2591f0cf fix(sidebar): rework the update notification [EE-4119] (#7574) 2022-08-30 10:00:15 +12:00
wheresolivia dacd188fa0 add data-cy attributes for docker image tag selectors (#7583) 2022-08-30 08:49:32 +12:00
Chaim Lev-Ari 4be094cb29 feat(ui): hide user menu on docker extension [EE-4115] (#7564) 2022-08-29 05:07:10 +03:00
congs 2c9328d765 fix(stack): EE-3908 broken modal when updating/redeploying stacks: turn off toggle (#7572) 2022-08-26 17:54:03 +12:00
Matt Hook ede9bc6111 fix(swarm): fixed issue parsing url with no scheme [EE-4017] (#7562) 2022-08-26 11:56:08 +12:00
itsconquest 2abb6650ae fix(stacks): orphaned stacks readonly [EE-4085] (#7551)
* fix(stacks): orphaned stacks readonly [EE-4085]

* correctly handle stack type in controller

* Update stackController.js
2022-08-25 10:57:43 +12:00
congs cc73724351 fix(container): EE-3995 gpus console error under stack list page (#7529) 2022-08-25 10:27:08 +12:00
Matt Hook 6fd5ebbf55 fix(web-editor): add search hint text [EE-3967] (#7495) 2022-08-25 10:11:10 +12:00
Chaim Lev-Ari aad0b139fc fix(stack): hide containers for swarm stack [EE-3969] (#7567) 2022-08-24 17:17:58 +03:00
Zhang Hao a8157caa67 fix(image): Add hide default registry teaser for CE version [EE-4038] (#7554) 2022-08-24 19:32:51 +08:00
Chaim Lev-Ari f69a6129e3 fix(ui/buttons): set hyperlink style [EE-4007] (#7523) 2022-08-24 07:40:43 +03:00
itsconquest e7d0bd6b65 fix(wizard): highcontrast style for BE only options (#7543) 2022-08-24 14:48:02 +12:00
itsconquest 956a6751a2 fix(azure): correctly sort container ports [EE-4076] (#7549) 2022-08-24 12:43:06 +12:00
congs de6ef0de83 fix(stack): EE-3908 broken modal when updating/redeploying stacks (#7498) 2022-08-23 14:22:27 +12:00
fhanportainer 8f8e89af3e fix(toggle): fixed disabled toggle color in dark and high contrast modes. (#7517)
* 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:15 +12:00
Prabhat Khera 9d45a25e99 fix minor ui issues (#7511) 2022-08-23 08:55:47 +12:00
Prabhat Khera c471b23bc5 fix(ui): minor ui issues EE-4004 (#7513) 2022-08-23 08:54:49 +12:00
fhanportainer 391d53cdc2 feat(label): uses --ui-white for control-label css class in Dark and High contrast themes (#7506)
* feat(label): uses --ui-white for control-label css class in Dark and High contrast themes.

* feat(label): uses tailwind to apply different colors in themes.

* feat(label): uses apply in control-label css class.
2022-08-23 03:56:21 +12:00
matias-portainer 3fe8697159 fix(edge): save edge checkin interval during endpoint creation EE-3958 (#7542) 2022-08-22 12:08:47 -03:00
Rex Wang 4dabe333f9 Fix(UI) fix color of file upload button in dark mode (release/2.15) EE-4009 (#7534)
* fix snapshot url parsing issue for ip addresses (#7478)

* fix(home): remove edge devices from homepage list EE-3919 (#7471)

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

* fix(activity): fix angularjs error [EE-3968] (#7483)

* fix(activity): fix angularjs error [EE-3968] (#7482)

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

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

Co-authored-by: Matt Hook <hookenz@gmail.com>
Co-authored-by: matias-portainer <104775949+matias-portainer@users.noreply.github.com>
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
2022-08-22 20:25:08 +08:00
Chaim Lev-Ari 05e3634594 fix(ui): box-selector fixes [EE-3949] (#7490) 2022-08-22 11:55:51 +03:00
Ali de3c75e701 fix(kubeshell): add data-cy to buttons EE-4054 (#7537) 2022-08-22 16:52:19 +12:00
fhanportainer de5dc93b5f fix(app-template): fixed the app template list not scroll to top issue. (#7520)
* 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:20 +12:00
fhanportainer 39896a6eaf feat(docker): fixed info icon in docker feature config section. (#7531) 2022-08-19 12:55:26 +12:00
matias-portainer 8b6bd59003 fix(home): remove edge devices from homepage list EE-3919 (#7525) 2022-08-18 10:25:19 -03:00
Rex Wang 750a37b861 EE-3998 bug fix (#7521) 2022-08-18 18:56:33 +08:00
Prabhat Khera 8a6593c7f5 fix background (#7516) 2022-08-18 22:40:01 +12:00
fhanportainer 6efd3c6a57 feat(stack): fixed stack web editor scroll bar issue. (#7532) 2022-08-18 18:00:31 +12:00
Rex Wang 8b21023c9d EE-3916 fix container link under stack detail page (#7508) 2022-08-17 23:48:15 +08:00
Rex Wang 0aebe38c31 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
2022-08-17 23:37:31 +08:00
Chaim Lev-Ari 28c4b333ce fix(containers): make table wider [EE-3944] (#7486) 2022-08-17 12:49:25 +03:00
Ali e4c7561dfc fix(kubeconfig): update button and modal styles (#7480)
EE-3947
2022-08-17 20:00:57 +12:00
Ali 750bb9cf87 fix(k8s/apps): show horizontal scrollbar [EE-3941] (#7476) 2022-08-16 20:58:55 +03:00
Chaim Lev-Ari c5f5269366 fix(ui/header): change font sizes [EE-3966] (#7484) 2022-08-16 18:08:04 +03:00
Matt Hook b9b8d78fcc fix snapshot url parsing issue for ip addresses (#7477) 2022-08-16 10:36:01 +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)
Test / test-client (push) Has been cancelled
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
1367 changed files with 31162 additions and 22861 deletions
+17 -2
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 }]
@@ -90,6 +104,7 @@ overrides:
'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:
+1
View File
@@ -7,6 +7,7 @@ storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json
.vscode
*.DS_Store
.eslintcache
+1 -1
View File
@@ -22,7 +22,7 @@ Please note that the public demo cluster is **reset every 15min**.
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.
**The latest version of Portainer is 2.13.x**.
## Getting started
+71
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
}
+9
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
+10 -1
View File
@@ -16,6 +16,7 @@ import (
"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"
@@ -743,7 +744,15 @@ func main() {
for {
server := buildServer(flags)
logrus.Printf("[INFO] [cmd,main] Starting Portainer version %s\n", portainer.APIVersion)
logrus.WithFields(logrus.Fields{
"Version": portainer.APIVersion,
"BuildNumber": build.BuildNumber,
"ImageTag": build.ImageTag,
"NodejsVersion": build.NodejsVersion,
"YarnVersion": build.YarnVersion,
"WebpackVersion": build.WebpackVersion,
"GoVersion": build.GoVersion},
).Print("[INFO] [cmd,main] Starting Portainer")
err := server.Start()
logrus.Printf("[INFO] [cmd,main] Http server exited: %v\n", err)
}
+19 -1
View File
@@ -103,8 +103,26 @@ func (store *Store) backupWithOptions(options *BackupOptions) (string, error) {
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
+3
View File
@@ -103,6 +103,9 @@ func (m *Migrator) Migrate() error {
// Portainer 2.14
newMigration(50, m.migrateDBVersionToDB50),
// Portainer 2.15
newMigration(60, m.migrateDBVersionToDB60),
}
var lastDbVersion int
@@ -0,0 +1,30 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) migrateDBVersionToDB60() error {
if err := m.addGpuInputFieldDB60(); err != nil {
return err
}
return nil
}
func (m *Migrator) addGpuInputFieldDB60() error {
migrateLog.Info("- add gpu input field")
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
for _, endpoint := range endpoints {
endpoint.Gpus = []portainer.Pair{}
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
if err != nil {
return err
}
}
return nil
}
@@ -27,6 +27,9 @@
],
"endpoints": [
{
"Agent": {
"Version": ""
},
"AuthorizedTeams": null,
"AuthorizedUsers": null,
"AzureCredentials": {
@@ -35,8 +38,15 @@
"TenantID": ""
},
"ComposeSyntaxMaxVersion": "",
"Edge": {
"AsyncMode": false,
"CommandInterval": 0,
"PingInterval": 0,
"SnapshotInterval": 0
},
"EdgeCheckinInterval": 0,
"EdgeKey": "",
"Gpus": [],
"GroupId": 1,
"Id": 1,
"IsEdgeDevice": false,
@@ -169,6 +179,8 @@
}
},
"DockerVersion": "20.10.13",
"GpuUseAll": false,
"GpuUseList": null,
"HealthyContainerCount": 0,
"ImageCount": 9,
"NodeCount": 0,
@@ -682,6 +694,12 @@
"BlackListedLabels": [],
"DisplayDonationHeader": false,
"DisplayExternalContributors": false,
"Edge": {
"AsyncMode": false,
"CommandInterval": 0,
"PingInterval": 0,
"SnapshotInterval": 0
},
"EdgeAgentCheckinInterval": 5,
"EdgePortainerUrl": "",
"EnableEdgeComputeFeatures": false,
@@ -778,6 +796,7 @@
"IsComposeFormat": false,
"Name": "alpine",
"Namespace": "",
"Option": null,
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/2",
"ResourceControl": null,
"Status": 1,
@@ -800,6 +819,7 @@
"IsComposeFormat": false,
"Name": "redis",
"Namespace": "",
"Option": null,
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/5",
"ResourceControl": null,
"Status": 1,
@@ -822,6 +842,7 @@
"IsComposeFormat": false,
"Name": "nginx",
"Namespace": "",
"Option": null,
"ProjectPath": "/home/prabhat/portainer/data/ce1.25/compose/6",
"ResourceControl": null,
"Status": 1,
@@ -898,7 +919,7 @@
],
"version": {
"DB_UPDATING": "false",
"DB_VERSION": "50",
"DB_VERSION": "61",
"INSTANCE_ID": "null"
}
}
+34 -1
View File
@@ -7,9 +7,10 @@ import (
"time"
"github.com/docker/docker/api/types"
_container "github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
// Snapshotter represents a service used to create environment(endpoint) snapshots
@@ -154,11 +155,35 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
healthyContainers := 0
unhealthyContainers := 0
stacks := make(map[string]struct{})
gpuUseSet := make(map[string]struct{})
gpuUseAll := false
for _, container := range containers {
if container.State == "exited" {
stoppedContainers++
} else if container.State == "running" {
runningContainers++
// snapshot GPUs
response, err := cli.ContainerInspect(context.Background(), container.ID)
if err != nil {
return err
}
var gpuOptions *_container.DeviceRequest = nil
for _, deviceRequest := range response.HostConfig.Resources.DeviceRequests {
if deviceRequest.Driver == "nvidia" || deviceRequest.Capabilities[0][0] == "gpu" {
gpuOptions = &deviceRequest
}
}
if gpuOptions != nil {
if gpuOptions.Count == -1 {
gpuUseAll = true
}
for _, id := range gpuOptions.DeviceIDs {
gpuUseSet[id] = struct{}{}
}
}
}
if strings.Contains(container.Status, "(healthy)") {
@@ -174,6 +199,14 @@ func snapshotContainers(snapshot *portainer.DockerSnapshot, cli *client.Client)
}
}
gpuUseList := make([]string, 0, len(gpuUseSet))
for gpuUse := range gpuUseSet {
gpuUseList = append(gpuUseList, gpuUse)
}
snapshot.GpuUseAll = gpuUseAll
snapshot.GpuUseList = gpuUseList
snapshot.RunningContainerCount = runningContainers
snapshot.StoppedContainerCount = stoppedContainers
snapshot.HealthyContainerCount = healthyContainers
+43 -180
View File
@@ -6,7 +6,6 @@ import (
"io"
"os"
"path"
"regexp"
"strings"
"github.com/pkg/errors"
@@ -14,7 +13,6 @@ import (
libstack "github.com/portainer/docker-compose-wrapper"
"github.com/portainer/docker-compose-wrapper/compose"
"github.com/docker/cli/cli/compose/loader"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
@@ -56,13 +54,13 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
defer proxy.Close()
}
envFilePath, err := createEnvFile(stack)
envFile, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath, forceRereate)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile, forceRereate)
return errors.Wrap(err, "failed to deploy a stack")
}
@@ -76,15 +74,38 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
defer proxy.Close()
}
if err := updateNetworkEnvFile(stack); err != nil {
return err
envFile, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
return errors.Wrap(err, "failed to remove a stack")
}
// Pull an image associated with a service defined in a docker-compose.yml or docker-stack.yml file,
// but does not start containers based on those images.
func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return err
}
if proxy != nil {
defer proxy.Close()
}
envFile, err := createEnvFile(stack)
if err != nil {
return errors.Wrap(err, "failed to create env file")
}
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Pull(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFile)
return errors.Wrap(err, "failed to pull images of the stack")
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
@@ -103,200 +124,42 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
return fmt.Sprintf("tcp://127.0.0.1:%d", proxy.Port), proxy, nil
}
// createEnvFile creates a file that would hold both "in-place" and default environment variables.
// It will return the name of the file if the stack has "in-place" env vars, otherwise empty string.
func createEnvFile(stack *portainer.Stack) (string, error) {
// workaround for EE-1862. It will have to be removed when
// docker/compose upgraded to v2.x.
if err := createNetworkEnvFile(stack); err != nil {
return "", errors.Wrap(err, "failed to create network env file")
}
if stack.Env == nil || len(stack.Env) == 0 {
return "", nil
}
envFilePath := path.Join(stack.ProjectPath, "stack.env")
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return "", err
}
defer envfile.Close()
copyDefaultEnvFile(stack, envfile)
for _, v := range stack.Env {
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
envfile.Close()
return "stack.env", nil
}
func fileNotExist(filePath string) bool {
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
return true
}
return false
}
func updateNetworkEnvFile(stack *portainer.Stack) error {
envFilePath := path.Join(stack.ProjectPath, ".env")
stackFilePath := path.Join(stack.ProjectPath, "stack.env")
if fileNotExist(envFilePath) {
if fileNotExist(stackFilePath) {
return nil
}
flags := os.O_WRONLY | os.O_SYNC | os.O_CREATE
envFile, err := os.OpenFile(envFilePath, flags, 0666)
if err != nil {
return err
}
defer envFile.Close()
stackFile, err := os.Open(stackFilePath)
if err != nil {
return err
}
defer stackFile.Close()
_, err = io.Copy(envFile, stackFile)
return err
}
return nil
}
func createNetworkEnvFile(stack *portainer.Stack) error {
networkNameSet := NewStringSet()
for _, filePath := range stackutils.GetStackFilePaths(stack) {
networkNames, err := extractNetworkNames(filePath)
if err != nil {
return errors.Wrap(err, "failed to extract network name")
}
if networkNames == nil || networkNames.Len() == 0 {
continue
}
networkNameSet.Union(networkNames)
}
for _, s := range networkNameSet.List() {
if _, ok := os.LookupEnv(s); ok {
networkNameSet.Remove(s)
}
}
if networkNameSet.Len() == 0 && stack.Env == nil {
return nil
}
envfile, err := os.OpenFile(path.Join(stack.ProjectPath, ".env"),
os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
// copyDefaultEnvFile copies the default .env file if it exists to the provided writer
func copyDefaultEnvFile(stack *portainer.Stack, w io.Writer) {
defaultEnvFile, err := os.Open(path.Join(path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint)), ".env"))
if err != nil {
return errors.Wrap(err, "failed to open env file")
// If cannot open a default file, then don't need to copy it.
// We could as well stat it and check if it exists, but this is more efficient.
return
}
defer envfile.Close()
defer defaultEnvFile.Close()
var scanEnvSettingFunc = func(name string) (string, bool) {
if stack.Env != nil {
for _, v := range stack.Env {
if name == v.Name {
return v.Value, true
}
}
}
return "", false
if _, err = io.Copy(w, defaultEnvFile); err == nil {
io.WriteString(w, "\n")
}
for _, s := range networkNameSet.List() {
if _, ok := scanEnvSettingFunc(s); !ok {
stack.Env = append(stack.Env, portainer.Pair{
Name: s,
Value: "None",
})
}
}
if stack.Env != nil {
for _, v := range stack.Env {
envfile.WriteString(
fmt.Sprintf("%s=%s\n", v.Name, v.Value))
}
}
return nil
}
func extractNetworkNames(filePath string) (StringSet, error) {
if info, err := os.Stat(filePath); errors.Is(err,
os.ErrNotExist) || info.IsDir() {
return nil, nil
}
stackFileContent, err := os.ReadFile(filePath)
if err != nil {
return nil, errors.Wrap(err, "failed to open yaml file")
}
config, err := loader.ParseYAML(stackFileContent)
if err != nil {
// invalid stack file
return nil, errors.Wrap(err, "invalid stack file")
}
var version string
if _, ok := config["version"]; ok {
version, _ = config["version"].(string)
}
var networks map[string]interface{}
if value, ok := config["networks"]; ok {
if value == nil {
return nil, nil
}
if networks, ok = value.(map[string]interface{}); !ok {
return nil, nil
}
} else {
return nil, nil
}
networkContent, err := loader.LoadNetworks(networks, version)
if err != nil {
return nil, nil // skip the error
}
re := regexp.MustCompile(`^\$\{?([^\}]+)\}?$`)
networkNames := NewStringSet()
for _, v := range networkContent {
matched := re.FindAllStringSubmatch(v.Name, -1)
if matched != nil && matched[0] != nil {
if strings.Contains(matched[0][1], ":-") {
continue
}
if strings.Contains(matched[0][1], "?") {
continue
}
if strings.Contains(matched[0][1], "-") {
continue
}
networkNames.Add(matched[0][1])
}
}
if networkNames.Len() == 0 {
return nil, nil
}
return networkNames, nil
// If couldn't copy the .env file, then ignore the error and try to continue
}
+12 -46
View File
@@ -65,56 +65,22 @@ func Test_createEnvFile(t *testing.T) {
}
}
func Test_createNetworkEnvFile(t *testing.T) {
func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
dir := t.TempDir()
buf := []byte(`
version: '3.6'
services:
nginx-example:
image: nginx:latest
networks:
default:
name: ${test}
driver: bridge
`)
if err := ioutil.WriteFile(path.Join(dir,
"docker-compose.yml"), buf, 0644); err != nil {
t.Fatalf("Failed to create yaml file: %s", err)
}
stackWithoutEnv := &portainer.Stack{
os.WriteFile(path.Join(dir, ".env"), []byte("VAR1=VAL1\nVAR2=VAL2\n"), 0600)
stack := &portainer.Stack{
ProjectPath: dir,
EntryPoint: "docker-compose.yml",
Env: []portainer.Pair{},
}
if err := createNetworkEnvFile(stackWithoutEnv); err != nil {
t.Fatalf("Failed to create network env file: %s", err)
}
content, err := ioutil.ReadFile(path.Join(dir, ".env"))
if err != nil {
t.Fatalf("Failed to read network env file: %s", err)
}
assert.Equal(t, "test=None\n", string(content))
stackWithEnv := &portainer.Stack{
ProjectPath: dir,
EntryPoint: "docker-compose.yml",
Env: []portainer.Pair{
{Name: "test", Value: "test-value"},
{Name: "VAR1", Value: "NEW_VAL1"},
{Name: "VAR3", Value: "VAL3"},
},
}
result, err := createEnvFile(stack)
assert.Equal(t, "stack.env", result)
assert.NoError(t, err)
assert.FileExists(t, path.Join(dir, "stack.env"))
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := ioutil.ReadAll(f)
if err := createNetworkEnvFile(stackWithEnv); err != nil {
t.Fatalf("Failed to create network env file: %s", err)
}
content, err = ioutil.ReadFile(path.Join(dir, ".env"))
if err != nil {
t.Fatalf("Failed to read network env file: %s", err)
}
assert.Equal(t, "test=test-value\n", string(content))
assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content)
}
+4 -1
View File
@@ -89,7 +89,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
}
// Deploy executes the docker stack deploy command.
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, pullImage bool, endpoint *portainer.Endpoint) error {
filePaths := stackutils.GetStackFilePaths(stack)
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
if err != nil {
@@ -101,6 +101,9 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
} else {
args = append(args, "stack", "deploy", "--with-registry-auth")
}
if !pullImage {
args = append(args, "--resolve-image=never")
}
args = configureFilePaths(args, filePaths)
args = append(args, stack.Name)
+10 -8
View File
@@ -1,6 +1,6 @@
module github.com/portainer/portainer/api
go 1.17
go 1.18
require (
github.com/Microsoft/go-winio v0.5.1
@@ -11,7 +11,7 @@ require (
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/docker/cli v20.10.9+incompatible
github.com/docker/docker v20.10.9+incompatible
github.com/docker/docker v20.10.16+incompatible
github.com/fvbommel/sortorder v1.0.2
github.com/fxamacker/cbor/v2 v2.3.0
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
@@ -20,7 +20,7 @@ require (
github.com/go-playground/validator/v10 v10.10.1
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-cmp v0.5.6
github.com/google/go-cmp v0.5.8
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
@@ -32,8 +32,8 @@ require (
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20220531190153-c597b853e410
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021
github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
@@ -43,6 +43,7 @@ require (
github.com/viney-shih/go-lock v1.1.1
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/alecthomas/kingpin.v2 v2.2.6
@@ -61,7 +62,6 @@ require (
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/containerd/containerd v1.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.0+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
@@ -95,6 +95,9 @@ require (
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.15.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -112,12 +115,11 @@ require (
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect
google.golang.org/grpc v1.43.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gotest.tools/v3 v3.0.3 // indirect
k8s.io/klog/v2 v2.30.0 // indirect
k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c // indirect
k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect
+10 -832
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,86 @@
package containers
import (
"net/http"
"strings"
containertypes "github.com/docker/docker/api/types/container"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portaineree "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
"golang.org/x/exp/slices"
)
type containerGpusResponse struct {
Gpus string `json:"gpus"`
}
// @id dockerContainerGpusInspect
// @summary Fetch container gpus data
// @description
// @description **Access policy**:
// @tags docker
// @security jwt
// @accept json
// @produce json
// @param environmentId path int true "Environment identifier"
// @param containerId path int true "Container identifier"
// @success 200 {object} containerGpusResponse "Success"
// @failure 404 "Environment or container not found"
// @failure 400 "Bad request"
// @failure 500 "Internal server error"
// @router /docker/{environmentId}/containers/{containerId}/gpus [get]
func (handler *Handler) containerGpusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
containerId, err := request.RetrieveRouteVariableValue(r, "containerId")
if err != nil {
return httperror.BadRequest("Invalid container identifier route variable", err)
}
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return httperror.NotFound("Unable to find an environment on request context", err)
}
agentTargetHeader := r.Header.Get(portaineree.PortainerAgentTargetHeader)
cli, err := handler.dockerClientFactory.CreateClient(endpoint, agentTargetHeader, nil)
if err != nil {
return httperror.InternalServerError("Unable to connect to the Docker daemon", err)
}
container, err := cli.ContainerInspect(r.Context(), containerId)
if err != nil {
return httperror.NotFound("Unable to find the container", err)
}
if container.HostConfig == nil {
return httperror.NotFound("Unable to find the container host config", err)
}
gpuOptionsIndex := slices.IndexFunc(container.HostConfig.DeviceRequests, func(opt containertypes.DeviceRequest) bool {
if opt.Driver == "nvidia" {
return true
}
if len(opt.Capabilities) == 0 || len(opt.Capabilities[0]) == 0 {
return false
}
return opt.Capabilities[0][0] == "gpu"
})
if gpuOptionsIndex == -1 {
return response.JSON(w, containerGpusResponse{Gpus: "none"})
}
gpuOptions := container.HostConfig.DeviceRequests[gpuOptionsIndex]
gpu := "all"
if gpuOptions.Count != -1 {
gpu = "id:" + strings.Join(gpuOptions.DeviceIDs, ",")
}
return response.JSON(w, containerGpusResponse{Gpus: gpu})
}
@@ -0,0 +1,31 @@
package containers
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/http/security"
)
type Handler struct {
*mux.Router
dockerClientFactory *docker.ClientFactory
}
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
func NewHandler(routePrefix string, bouncer *security.RequestBouncer, dockerClientFactory *docker.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
dockerClientFactory: dockerClientFactory,
}
router := h.PathPrefix(routePrefix).Subrouter()
router.Use(bouncer.AuthenticatedAccess)
router.Handle("/{containerId}/gpus", httperror.LoggerHandler(h.containerGpusInspect)).Methods(http.MethodGet)
return h
}
+63
View File
@@ -0,0 +1,63 @@
package docker
import (
"errors"
"net/http"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/handler/docker/containers"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
)
// Handler is the HTTP handler which will natively deal with to external environments(endpoints).
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
dataStore dataservices.DataStore
dockerClientFactory *docker.ClientFactory
authorizationService *authorization.Service
}
// NewHandler creates a handler to process non-proxied requests to docker APIs directly.
func NewHandler(bouncer *security.RequestBouncer, authorizationService *authorization.Service, dataStore dataservices.DataStore, dockerClientFactory *docker.ClientFactory) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
authorizationService: authorizationService,
dataStore: dataStore,
dockerClientFactory: dockerClientFactory,
}
// endpoints
endpointRouter := h.PathPrefix("/{id}").Subrouter()
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
endpointRouter.Use(dockerOnlyMiddleware)
containersHandler := containers.NewHandler("/{id}/containers", bouncer, dockerClientFactory)
endpointRouter.PathPrefix("/containers").Handler(containersHandler)
return h
}
func dockerOnlyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) {
endpoint, err := middlewares.FetchEndpoint(request)
if err != nil {
httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err)
return
}
if !endpointutils.IsDockerEndpoint(endpoint) {
errMessage := "environment is not a docker environment"
httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage))
return
}
next.ServeHTTP(rw, request)
})
}
@@ -77,14 +77,17 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
if endpoint.EdgeID == "" {
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
endpoint.EdgeID = edgeIdentifier
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
if agentPlatformErr != nil {
return httperror.BadRequest("agent platform header is not valid", err)
}
endpoint.Type = agentPlatform
}
agentPlatform, agentPlatformErr := parseAgentPlatform(r)
if agentPlatformErr != nil {
return httperror.BadRequest("agent platform header is not valid", err)
}
endpoint.Type = agentPlatform
version := r.Header.Get(portainer.PortainerAgentHeader)
endpoint.Agent.Version = version
endpoint.LastCheckInDate = time.Now().Unix()
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
@@ -57,7 +57,7 @@ var endpointTestCases = []endpointTestCase{
portainer.EndpointRelation{
EndpointID: 2,
},
http.StatusBadRequest,
http.StatusForbidden,
},
{
portainer.Endpoint{
@@ -194,7 +194,9 @@ func TestWithEndpoints(t *testing.T) {
if err != nil {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, test.endpoint.EdgeID)
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -239,6 +241,7 @@ func TestLastCheckInDateIncreases(t *testing.T) {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -355,6 +358,7 @@ func TestEdgeStackStatus(t *testing.T) {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -424,6 +428,7 @@ func TestEdgeJobsResponse(t *testing.T) {
t.Fatal("request error:", err)
}
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
req.Header.Set(portainer.HTTPResponseAgentPlatform, "1")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
@@ -0,0 +1,50 @@
package endpoints
import (
"net/http"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/set"
)
// @id AgentVersions
// @summary List agent versions
// @description List all agent versions based on the current user authorizations and query parameters.
// @description **Access policy**: restricted
// @tags endpoints
// @security ApiKeyAuth
// @security jwt
// @produce json
// @success 200 {array} string "List of available agent versions"
// @failure 500 "Server error"
// @router /endpoints/agent_versions [get]
func (handler *Handler) agentVersions(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
}
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
agentVersions := set.Set[string]{}
for _, endpoint := range filteredEndpoints {
if endpoint.Agent.Version != "" {
agentVersions[endpoint.Agent.Version] = true
}
}
return response.JSON(w, agentVersions.Keys())
}
+31 -61
View File
@@ -1,20 +1,19 @@
package endpoints
import (
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/url"
"runtime"
"strconv"
"strings"
"time"
"github.com/gofrs/uuid"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/agent"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/internal/edge"
@@ -25,6 +24,7 @@ type endpointCreatePayload struct {
URL string
EndpointCreationType endpointCreationEnum
PublicURL string
Gpus []portainer.Pair
GroupID int
TLS bool
TLSSkipVerify bool
@@ -142,6 +142,13 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
payload.PublicURL = publicURL
}
gpus := make([]portainer.Pair, 0)
err = request.RetrieveMultiPartFormJSONValue(r, "Gpus", &gpus, true)
if err != nil {
return errors.New("Invalid Gpus parameter")
}
payload.Gpus = gpus
checkinInterval, _ := request.RetrieveNumericMultiPartFormValue(r, "CheckinInterval", true)
payload.EdgeCheckinInterval = checkinInterval
@@ -237,6 +244,7 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
}
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
var err error
switch payload.EndpointCreationType {
case azureEnvironment:
return handler.createAzureEndpoint(payload)
@@ -249,12 +257,22 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
}
endpointType := portainer.DockerEnvironment
var agentVersion string
if payload.EndpointCreationType == agentEnvironment {
agentPlatform, err := handler.pingAndCheckPlatform(payload)
var tlsConfig *tls.Config
if payload.TLS {
tlsConfig, err = crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
if err != nil {
return nil, httperror.InternalServerError("Unable to create TLS configuration", err)
}
}
agentPlatform, version, err := agent.GetAgentVersionAndPlatform(payload.URL, tlsConfig)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get environment type", err}
}
agentVersion = version
if agentPlatform == portainer.AgentPlatformDocker {
endpointType = portainer.AgentOnDockerEnvironment
} else if agentPlatform == portainer.AgentPlatformKubernetes {
@@ -264,7 +282,7 @@ func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portain
}
if payload.TLS {
return handler.createTLSSecuredEndpoint(payload, endpointType)
return handler.createTLSSecuredEndpoint(payload, endpointType, agentVersion)
}
return handler.createUnsecuredEndpoint(payload)
}
@@ -290,6 +308,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
Type: portainer.AzureEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
UserAccessPolicies: portainer.UserAccessPolicies{},
TeamAccessPolicies: portainer.TeamAccessPolicies{},
AzureCredentials: credentials,
@@ -323,6 +342,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
URL: portainerHost,
Type: portainer.EdgeAgentOnDockerEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
@@ -378,6 +398,7 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
@@ -412,6 +433,7 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
Type: portainer.KubernetesLocalEnvironment,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.TLS,
TLSSkipVerify: payload.TLSSkipVerify,
@@ -432,7 +454,7 @@ func (handler *Handler) createKubernetesEndpoint(payload *endpointCreatePayload)
return endpoint, nil
}
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) {
func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType, agentVersion string) (*portainer.Endpoint, *httperror.HandlerError) {
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
@@ -441,6 +463,7 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
PublicURL: payload.PublicURL,
Gpus: payload.Gpus,
TLSConfig: portainer.TLSConfiguration{
TLS: payload.TLS,
TLSSkipVerify: payload.TLSSkipVerify,
@@ -454,6 +477,8 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload,
IsEdgeDevice: payload.IsEdgeDevice,
}
endpoint.Agent.Version = agentVersion
err := handler.storeTLSFiles(endpoint, payload)
if err != nil {
return nil, err
@@ -547,58 +572,3 @@ func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *end
return nil
}
func (handler *Handler) pingAndCheckPlatform(payload *endpointCreatePayload) (portainer.AgentPlatform, error) {
httpCli := &http.Client{
Timeout: 3 * time.Second,
}
if payload.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
if err != nil {
return 0, err
}
httpCli.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
url, err := url.Parse(fmt.Sprintf("%s/ping", payload.URL))
if err != nil {
return 0, err
}
url.Scheme = "https"
req, err := http.NewRequest(http.MethodGet, url.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)
}
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), nil
}
+24 -335
View File
@@ -4,24 +4,14 @@ import (
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/utils"
)
const (
EdgeDeviceFilterAll = "all"
EdgeDeviceFilterTrusted = "trusted"
EdgeDeviceFilterUntrusted = "untrusted"
EdgeDeviceFilterNone = "none"
)
const (
@@ -29,8 +19,6 @@ const (
EdgeDeviceIntervalAdd = 20
)
var endpointGroupNames map[portainer.EndpointGroupID]string
// @id EndpointList
// @summary List environments(endpoints)
// @description List all environments(endpoints) based on the current user authorizations. Will
@@ -42,14 +30,20 @@ var endpointGroupNames map[portainer.EndpointGroupID]string
// @security jwt
// @produce json
// @param start query int false "Start searching from"
// @param search query string false "Search query"
// @param groupId query int false "List environments(endpoints) of this group"
// @param limit query int false "Limit results to this value"
// @param sort query int false "Sort results by this value"
// @param order query int false "Order sorted results by desc/asc" Enum("asc", "desc")
// @param search query string false "Search query"
// @param groupIds query []int false "List environments(endpoints) of these groups"
// @param status query []int false "List environments(endpoints) by this status"
// @param types query []int false "List environments(endpoints) of this type"
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
// @param endpointIds query []int false "will return only these environments(endpoints)"
// @param edgeDeviceFilter query string false "will return only these edge environments, none will return only regular edge environments" Enum("all", "trusted", "untrusted", "none")
// @param provisioned query bool false "If true, will return environment(endpoint) that were provisioned"
// @param agentVersions query []string false "will return only environments with on of these agent versions"
// @param edgeDevice query bool false "if exists true show only edge devices, false show only regular edge endpoints. if missing, will show both types (relevant only for edge endpoints)"
// @param edgeDeviceUntrusted query bool false "if true, show only untrusted endpoints, if false show only trusted (relevant only for edge devices, and if edgeDevice is true)"
// @param name query string false "will return only environments(endpoints) with this name"
// @success 200 {array} portainer.Endpoint "Endpoints"
// @failure 500 "Server error"
@@ -60,103 +54,42 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
start--
}
search, _ := request.RetrieveQueryParameter(r, "search", true)
if search != "" {
search = strings.ToLower(search)
}
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
sortField, _ := request.RetrieveQueryParameter(r, "sort", true)
sortOrder, _ := request.RetrieveQueryParameter(r, "order", true)
var endpointTypes []int
request.RetrieveJSONQueryParameter(r, "types", &endpointTypes, true)
var tagIDs []portainer.TagID
request.RetrieveJSONQueryParameter(r, "tagIds", &tagIDs, true)
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
var endpointIDs []portainer.EndpointID
request.RetrieveJSONQueryParameter(r, "endpointIds", &endpointIDs, true)
var statuses []int
request.RetrieveJSONQueryParameter(r, "status", &statuses, true)
var groupIDs []int
request.RetrieveJSONQueryParameter(r, "groupIds", &groupIDs, true)
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
return httperror.InternalServerError("Unable to retrieve environment groups from the database", err)
}
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
return httperror.InternalServerError("Unable to retrieve environments from the database", err)
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
return httperror.InternalServerError("Unable to retrieve settings from the database", err)
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
return httperror.InternalServerError("Unable to retrieve info from request context", err)
}
query, err := parseQuery(r)
if err != nil {
return httperror.BadRequest("Invalid query parameters", err)
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
totalAvailableEndpoints := len(filteredEndpoints)
if groupID != 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, []int{groupID})
filteredEndpoints, totalAvailableEndpoints, err := handler.filterEndpointsByQuery(filteredEndpoints, query, endpointGroups, settings)
if err != nil {
return httperror.InternalServerError("Unable to filter endpoints", err)
}
if endpointIDs != nil {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
}
if len(groupIDs) > 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, groupIDs)
}
name, _ := request.RetrieveQueryParameter(r, "name", true)
if name != "" {
filteredEndpoints = filterEndpointsByName(filteredEndpoints, name)
}
edgeDeviceFilter, _ := request.RetrieveQueryParameter(r, "edgeDeviceFilter", false)
if edgeDeviceFilter != "" {
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
}
if len(statuses) > 0 {
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, statuses, settings)
}
if search != "" {
tags, err := handler.DataStore.Tag().Tags()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve tags from the database", err}
}
tagsMap := make(map[portainer.TagID]string)
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, tagsMap, search)
}
if endpointTypes != nil {
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, endpointTypes)
}
if tagIDs != nil {
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, tagIDs, endpointGroups, tagsPartialMatch)
}
// Sort endpoints by field
sortEndpointsByField(filteredEndpoints, endpointGroups, sortField, sortOrder == "desc")
filteredEndpointCount := len(filteredEndpoints)
@@ -196,64 +129,6 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta
return endpoints[start:end]
}
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []int) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if utils.Contains(endpointGroupIDs, int(endpoint.GroupID)) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
continue
}
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []int, settings *portainer.Settings) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
status := endpoint.Status
if endpointutils.IsEdgeEndpoint(&endpoint) {
isCheckValid := false
edgeCheckinInterval := endpoint.EdgeCheckinInterval
if endpoint.EdgeCheckinInterval == 0 {
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
}
status = portainer.EndpointStatusDown // Offline
if isCheckValid {
status = portainer.EndpointStatusUp // Online
}
}
if utils.Contains(statuses, int(status)) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, sortField string, isSortDesc bool) {
switch sortField {
@@ -265,7 +140,7 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []porta
}
case "Group":
endpointGroupNames = make(map[portainer.EndpointGroupID]string, 0)
endpointGroupNames := make(map[portainer.EndpointGroupID]string, 0)
for _, group := range endpointGroups {
endpointGroupNames[group.ID] = group.Name
}
@@ -294,123 +169,6 @@ func sortEndpointsByField(endpoints []portainer.Endpoint, endpointGroups []porta
}
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
return true
}
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
return true
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true
}
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
return false
}
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
}
}
return false
}
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []int) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
typeSet := map[portainer.EndpointType]bool{}
for _, endpointType := range endpointTypes {
typeSet[portainer.EndpointType(endpointType)] = true
}
for _, endpoint := range endpoints {
if typeSet[endpoint.Type] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if shouldReturnEdgeDevice(endpoint, edgeDeviceFilter) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceFilter string) bool {
// none - return all endpoints that are not edge devices
if edgeDeviceFilter == EdgeDeviceFilterNone && !endpoint.IsEdgeDevice {
return true
}
if !endpointutils.IsEdgeEndpoint(&endpoint) {
return false
}
switch edgeDeviceFilter {
case EdgeDeviceFilterAll:
return true
case EdgeDeviceFilterTrusted:
return endpoint.UserTrusted
case EdgeDeviceFilterUntrusted:
return !endpoint.UserTrusted
}
return false
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
endpointMatched := false
if partialMatch {
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
} else {
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
}
if endpointMatched {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.EndpointGroup) portainer.EndpointGroup {
var endpointGroup portainer.EndpointGroup
for _, group := range groups {
@@ -421,72 +179,3 @@ func getEndpointGroup(groupID portainer.EndpointGroupID, groups []portainer.Endp
}
return endpointGroup
}
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
tagSet := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
tagSet[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if tagSet[tagID] {
return true
}
}
for _, tagID := range endpointGroup.TagIDs {
if tagSet[tagID] {
return true
}
}
return false
}
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
missingTags := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
missingTags[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
for _, tagID := range endpointGroup.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
return len(missingTags) == 0
}
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
idsSet := make(map[portainer.EndpointID]bool)
for _, id := range ids {
idsSet[id] = true
}
for _, endpoint := range endpoints {
if idsSet[endpoint.ID] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portainer.Endpoint {
if name == "" {
return endpoints
}
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if endpoint.Name == name {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
+146 -40
View File
@@ -16,66 +16,147 @@ import (
"github.com/stretchr/testify/assert"
)
type endpointListEdgeDeviceTest struct {
type endpointListTest struct {
title string
expected []portainer.EndpointID
filter string
}
func Test_endpointList(t *testing.T) {
var err error
is := assert.New(t)
func Test_EndpointList_AgentVersion(t *testing.T) {
version1Endpoint := portainer.Endpoint{
ID: 1,
GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
Agent: struct {
Version string "example:\"1.0.0\""
}{
Version: "1.0.0",
},
}
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1, Type: portainer.AgentOnDockerEnvironment, Agent: struct {
Version string "example:\"1.0.0\""
}{Version: "2.0.0"}}
noVersionEndpoint := portainer.Endpoint{ID: 3, Type: portainer.AgentOnDockerEnvironment, GroupID: 1}
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
handler, teardown := setup(t, []portainer.Endpoint{
notAgentEnvironments,
version1Endpoint,
version2Endpoint,
noVersionEndpoint,
})
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
trustedEndpoint := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
untrustedEndpoint := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
type endpointListAgentVersionTest struct {
endpointListTest
filter []string
}
tests := []endpointListAgentVersionTest{
{
endpointListTest{
"should show version 1 agent endpoints and non-agent endpoints",
[]portainer.EndpointID{version1Endpoint.ID, notAgentEnvironments.ID},
},
[]string{version1Endpoint.Agent.Version},
},
{
endpointListTest{
"should show version 2 endpoints and non-agent endpoints",
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID},
},
[]string{version2Endpoint.Agent.Version},
},
{
endpointListTest{
"should show version 1 and 2 endpoints and non-agent endpoints",
[]portainer.EndpointID{version2Endpoint.ID, notAgentEnvironments.ID, version1Endpoint.ID},
},
[]string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
},
}
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
is := assert.New(t)
query := ""
for _, filter := range test.filter {
query += fmt.Sprintf("agentVersions[]=%s&", filter)
}
req := buildEndpointListRequest(query)
resp, err := doEndpointListRequest(req, handler, is)
is.NoError(err)
is.Equal(len(test.expected), len(resp))
respIds := []portainer.EndpointID{}
for _, endpoint := range resp {
respIds = append(respIds, endpoint.ID)
}
is.ElementsMatch(test.expected, respIds)
})
}
}
func Test_endpointList_edgeDeviceFilter(t *testing.T) {
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularEndpoint := portainer.Endpoint{ID: 5, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.DockerEnvironment}
endpoints := []portainer.Endpoint{
trustedEndpoint,
untrustedEndpoint,
handler, teardown := setup(t, []portainer.Endpoint{
trustedEdgeDevice,
untrustedEdgeDevice,
regularUntrustedEdgeEndpoint,
regularTrustedEdgeEndpoint,
regularEndpoint,
})
defer teardown()
type endpointListEdgeDeviceTest struct {
endpointListTest
edgeDevice *bool
edgeDeviceUntrusted bool
}
for _, endpoint := range endpoints {
err = store.Endpoint().Create(&endpoint)
is.NoError(err, "error creating environment")
}
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
h := NewHandler(bouncer, nil)
h.DataStore = store
h.ComposeStackManager = testhelpers.NewComposeStackManager()
tests := []endpointListEdgeDeviceTest{
{
"should show all edge endpoints",
[]portainer.EndpointID{trustedEndpoint.ID, untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EdgeDeviceFilterAll,
endpointListTest: endpointListTest{
"should show all endpoints except of the untrusted devices",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID, regularEndpoint.ID},
},
edgeDevice: nil,
},
{
"should show only trusted edge devices",
[]portainer.EndpointID{trustedEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EdgeDeviceFilterTrusted,
endpointListTest: endpointListTest{
"should show only trusted edge devices and regular endpoints",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
},
edgeDevice: BoolAddr(true),
},
{
"should show only untrusted edge devices",
[]portainer.EndpointID{untrustedEndpoint.ID, regularUntrustedEdgeEndpoint.ID},
EdgeDeviceFilterUntrusted,
endpointListTest: endpointListTest{
"should show only untrusted edge devices and regular endpoints",
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
},
edgeDevice: BoolAddr(true),
edgeDeviceUntrusted: true,
},
{
"should show no edge devices",
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EdgeDeviceFilterNone,
endpointListTest: endpointListTest{
"should show no edge devices",
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
},
edgeDevice: BoolAddr(false),
},
}
@@ -83,8 +164,13 @@ func Test_endpointList(t *testing.T) {
t.Run(test.title, func(t *testing.T) {
is := assert.New(t)
req := buildEndpointListRequest(test.filter)
resp, err := doEndpointListRequest(req, h, is)
query := fmt.Sprintf("edgeDeviceUntrusted=%v&", test.edgeDeviceUntrusted)
if test.edgeDevice != nil {
query += fmt.Sprintf("edgeDevice=%v&", *test.edgeDevice)
}
req := buildEndpointListRequest(query)
resp, err := doEndpointListRequest(req, handler, is)
is.NoError(err)
is.Equal(len(test.expected), len(resp))
@@ -100,8 +186,28 @@ func Test_endpointList(t *testing.T) {
}
}
func buildEndpointListRequest(filter string) *http.Request {
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?edgeDeviceFilter=%s", filter), nil)
func setup(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
for _, endpoint := range endpoints {
err := store.Endpoint().Create(&endpoint)
is.NoError(err, "error creating environment")
}
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
handler = NewHandler(bouncer, nil)
handler.DataStore = store
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
return handler, teardown
}
func buildEndpointListRequest(query string) *http.Request {
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?%s", query), nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)
@@ -55,6 +55,7 @@ func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request)
latestEndpointReference.Snapshots = endpoint.Snapshots
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
latestEndpointReference.Agent.Version = endpoint.Agent.Version
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {
@@ -47,6 +47,7 @@ func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request
latestEndpointReference.Snapshots = endpoint.Snapshots
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
latestEndpointReference.Agent.Version = endpoint.Agent.Version
err = handler.DataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {
@@ -22,6 +22,8 @@ type endpointUpdatePayload struct {
// URL or IP address where exposed containers will be reachable.\
// Defaults to URL if not specified
PublicURL *string `example:"docker.mydomain.tld:2375"`
// GPUs information
Gpus []portainer.Pair
// Group identifier
GroupID *int `example:"1"`
// Require TLS to connect against this environment(endpoint)
@@ -110,6 +112,10 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.PublicURL = *payload.PublicURL
}
if payload.Gpus != nil {
endpoint.Gpus = payload.Gpus
}
if payload.EdgeCheckinInterval != nil {
endpoint.EdgeCheckinInterval = *payload.EdgeCheckinInterval
}
@@ -265,7 +271,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
if (payload.URL != nil && *payload.URL != endpoint.URL) || (payload.TLS != nil && endpoint.TLSConfig.TLS != *payload.TLS) || endpoint.Type == portainer.AzureEnvironment {
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
if err != nil {
+435
View File
@@ -0,0 +1,435 @@
package endpoints
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
"golang.org/x/exp/slices"
)
type EnvironmentsQuery struct {
search string
types []portainer.EndpointType
tagIds []portainer.TagID
endpointIds []portainer.EndpointID
tagsPartialMatch bool
groupIds []portainer.EndpointGroupID
status []portainer.EndpointStatus
edgeDevice *bool
edgeDeviceUntrusted bool
name string
agentVersions []string
}
func parseQuery(r *http.Request) (EnvironmentsQuery, error) {
search, _ := request.RetrieveQueryParameter(r, "search", true)
if search != "" {
search = strings.ToLower(search)
}
status, err := getNumberArrayQueryParameter[portainer.EndpointStatus](r, "status")
if err != nil {
return EnvironmentsQuery{}, err
}
groupIDs, err := getNumberArrayQueryParameter[portainer.EndpointGroupID](r, "groupIds")
if err != nil {
return EnvironmentsQuery{}, err
}
endpointTypes, err := getNumberArrayQueryParameter[portainer.EndpointType](r, "types")
if err != nil {
return EnvironmentsQuery{}, err
}
tagIDs, err := getNumberArrayQueryParameter[portainer.TagID](r, "tagIds")
if err != nil {
return EnvironmentsQuery{}, err
}
tagsPartialMatch, _ := request.RetrieveBooleanQueryParameter(r, "tagsPartialMatch", true)
endpointIDs, err := getNumberArrayQueryParameter[portainer.EndpointID](r, "endpointIds")
if err != nil {
return EnvironmentsQuery{}, err
}
agentVersions := getArrayQueryParameter(r, "agentVersions")
name, _ := request.RetrieveQueryParameter(r, "name", true)
edgeDeviceParam, _ := request.RetrieveQueryParameter(r, "edgeDevice", true)
var edgeDevice *bool
if edgeDeviceParam != "" {
edgeDevice = BoolAddr(edgeDeviceParam == "true")
}
edgeDeviceUntrusted, _ := request.RetrieveBooleanQueryParameter(r, "edgeDeviceUntrusted", true)
return EnvironmentsQuery{
search: search,
types: endpointTypes,
tagIds: tagIDs,
endpointIds: endpointIDs,
tagsPartialMatch: tagsPartialMatch,
groupIds: groupIDs,
status: status,
edgeDevice: edgeDevice,
edgeDeviceUntrusted: edgeDeviceUntrusted,
name: name,
agentVersions: agentVersions,
}, nil
}
func (handler *Handler) filterEndpointsByQuery(filteredEndpoints []portainer.Endpoint, query EnvironmentsQuery, groups []portainer.EndpointGroup, settings *portainer.Settings) ([]portainer.Endpoint, int, error) {
totalAvailableEndpoints := len(filteredEndpoints)
if len(query.endpointIds) > 0 {
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds)
}
if len(query.groupIds) > 0 {
filteredEndpoints = filterEndpointsByGroupIDs(filteredEndpoints, query.groupIds)
}
if query.name != "" {
filteredEndpoints = filterEndpointsByName(filteredEndpoints, query.name)
}
if query.edgeDevice != nil {
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, *query.edgeDevice, query.edgeDeviceUntrusted)
} else {
// If the edgeDevice parameter is not set, we need to filter out the untrusted edge devices
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
return !endpoint.IsEdgeDevice || endpoint.UserTrusted
})
}
if len(query.status) > 0 {
filteredEndpoints = filterEndpointsByStatuses(filteredEndpoints, query.status, settings)
}
if query.search != "" {
tags, err := handler.DataStore.Tag().Tags()
if err != nil {
return nil, 0, errors.WithMessage(err, "Unable to retrieve tags from the database")
}
tagsMap := make(map[portainer.TagID]string)
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, groups, tagsMap, query.search)
}
if len(query.types) > 0 {
filteredEndpoints = filterEndpointsByTypes(filteredEndpoints, query.types)
}
if len(query.tagIds) > 0 {
filteredEndpoints = filteredEndpointsByTags(filteredEndpoints, query.tagIds, groups, query.tagsPartialMatch)
}
if len(query.agentVersions) > 0 {
filteredEndpoints = filter(filteredEndpoints, func(endpoint portainer.Endpoint) bool {
return !endpointutils.IsAgentEndpoint(&endpoint) || contains(query.agentVersions, endpoint.Agent.Version)
})
}
return filteredEndpoints, totalAvailableEndpoints, nil
}
func filterEndpointsByGroupIDs(endpoints []portainer.Endpoint, endpointGroupIDs []portainer.EndpointGroupID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if slices.Contains(endpointGroupIDs, endpoint.GroupID) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
continue
}
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, tagsMap, searchCriteria) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portainer.EndpointStatus, settings *portainer.Settings) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
status := endpoint.Status
if endpointutils.IsEdgeEndpoint(&endpoint) {
isCheckValid := false
edgeCheckinInterval := endpoint.EdgeCheckinInterval
if endpoint.EdgeCheckinInterval == 0 {
edgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
if edgeCheckinInterval != 0 && endpoint.LastCheckInDate != 0 {
isCheckValid = time.Now().Unix()-endpoint.LastCheckInDate <= int64(edgeCheckinInterval*EdgeDeviceIntervalMultiplier+EdgeDeviceIntervalAdd)
}
status = portainer.EndpointStatusDown // Offline
if isCheckValid {
status = portainer.EndpointStatusUp // Online
}
}
if slices.Contains(statuses, status) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
return true
}
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
return true
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
return true
}
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
return false
}
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
}
}
}
return false
}
func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []portainer.EndpointType) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
typeSet := map[portainer.EndpointType]bool{}
for _, endpointType := range endpointTypes {
typeSet[portainer.EndpointType(endpointType)] = true
}
for _, endpoint := range endpoints {
if typeSet[endpoint.Type] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDevice bool, untrusted bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if shouldReturnEdgeDevice(endpoint, edgeDevice, untrusted) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceParam bool, untrustedParam bool) bool {
if !endpointutils.IsEdgeEndpoint(&endpoint) {
return true
}
if !edgeDeviceParam {
return !endpoint.IsEdgeDevice
}
return endpoint.IsEdgeDevice && endpoint.UserTrusted == !untrustedParam
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
endpointGroup := getEndpointGroup(endpoint.GroupID, endpointGroups)
endpointMatched := false
if partialMatch {
endpointMatched = endpointPartialMatchTags(endpoint, endpointGroup, tagIDs)
} else {
endpointMatched = endpointFullMatchTags(endpoint, endpointGroup, tagIDs)
}
if endpointMatched {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func endpointPartialMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
tagSet := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
tagSet[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if tagSet[tagID] {
return true
}
}
for _, tagID := range endpointGroup.TagIDs {
if tagSet[tagID] {
return true
}
}
return false
}
func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.EndpointGroup, tagIDs []portainer.TagID) bool {
missingTags := make(map[portainer.TagID]bool)
for _, tagID := range tagIDs {
missingTags[tagID] = true
}
for _, tagID := range endpoint.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
for _, tagID := range endpointGroup.TagIDs {
if missingTags[tagID] {
delete(missingTags, tagID)
}
}
return len(missingTags) == 0
}
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
idsSet := make(map[portainer.EndpointID]bool)
for _, id := range ids {
idsSet[id] = true
}
for _, endpoint := range endpoints {
if idsSet[endpoint.ID] {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filterEndpointsByName(endpoints []portainer.Endpoint, name string) []portainer.Endpoint {
if name == "" {
return endpoints
}
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if endpoint.Name == name {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func filter(endpoints []portainer.Endpoint, predicate func(endpoint portainer.Endpoint) bool) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
if predicate(endpoint) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func getArrayQueryParameter(r *http.Request, parameter string) []string {
list, exists := r.Form[fmt.Sprintf("%s[]", parameter)]
if !exists {
list = []string{}
}
return list
}
func getNumberArrayQueryParameter[T ~int](r *http.Request, parameter string) ([]T, error) {
list := getArrayQueryParameter(r, parameter)
if list == nil {
return []T{}, nil
}
var result []T
for _, item := range list {
number, err := strconv.Atoi(item)
if err != nil {
return nil, errors.Wrapf(err, "Unable to parse parameter %s", parameter)
}
result = append(result, T(number))
}
return result, nil
}
func contains(strings []string, param string) bool {
for _, str := range strings {
if str == param {
return true
}
}
return false
}
+177
View File
@@ -0,0 +1,177 @@
package endpoints
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/internal/testhelpers"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
type filterTest struct {
title string
expected []portainer.EndpointID
query EnvironmentsQuery
}
func Test_Filter_AgentVersion(t *testing.T) {
version1Endpoint := portainer.Endpoint{ID: 1, GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
Agent: struct {
Version string "example:\"1.0.0\""
}{Version: "1.0.0"}}
version2Endpoint := portainer.Endpoint{ID: 2, GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
Agent: struct {
Version string "example:\"1.0.0\""
}{Version: "2.0.0"}}
noVersionEndpoint := portainer.Endpoint{ID: 3, GroupID: 1,
Type: portainer.AgentOnDockerEnvironment,
}
notAgentEnvironments := portainer.Endpoint{ID: 4, Type: portainer.DockerEnvironment, GroupID: 1}
endpoints := []portainer.Endpoint{
version1Endpoint,
version2Endpoint,
noVersionEndpoint,
notAgentEnvironments,
}
handler, teardown := setupFilterTest(t, endpoints)
defer teardown()
tests := []filterTest{
{
"should show version 1 endpoints",
[]portainer.EndpointID{version1Endpoint.ID},
EnvironmentsQuery{
agentVersions: []string{version1Endpoint.Agent.Version},
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
},
},
{
"should show version 2 endpoints",
[]portainer.EndpointID{version2Endpoint.ID},
EnvironmentsQuery{
agentVersions: []string{version2Endpoint.Agent.Version},
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
},
},
{
"should show version 1 and 2 endpoints",
[]portainer.EndpointID{version2Endpoint.ID, version1Endpoint.ID},
EnvironmentsQuery{
agentVersions: []string{version2Endpoint.Agent.Version, version1Endpoint.Agent.Version},
types: []portainer.EndpointType{portainer.AgentOnDockerEnvironment},
},
},
}
runTests(tests, t, handler, endpoints)
}
func Test_Filter_edgeDeviceFilter(t *testing.T) {
trustedEdgeDevice := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
untrustedEdgeDevice := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularUntrustedEdgeEndpoint := portainer.Endpoint{ID: 3, UserTrusted: false, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularTrustedEdgeEndpoint := portainer.Endpoint{ID: 4, UserTrusted: true, IsEdgeDevice: false, GroupID: 1, Type: portainer.EdgeAgentOnDockerEnvironment}
regularEndpoint := portainer.Endpoint{ID: 5, GroupID: 1, Type: portainer.DockerEnvironment}
endpoints := []portainer.Endpoint{
trustedEdgeDevice,
untrustedEdgeDevice,
regularUntrustedEdgeEndpoint,
regularTrustedEdgeEndpoint,
regularEndpoint,
}
handler, teardown := setupFilterTest(t, endpoints)
defer teardown()
tests := []filterTest{
{
"should show all edge endpoints except of the untrusted devices",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EnvironmentsQuery{
types: []portainer.EndpointType{portainer.EdgeAgentOnDockerEnvironment, portainer.EdgeAgentOnKubernetesEnvironment},
},
},
{
"should show only trusted edge devices and other regular endpoints",
[]portainer.EndpointID{trustedEdgeDevice.ID, regularEndpoint.ID},
EnvironmentsQuery{
edgeDevice: BoolAddr(true),
},
},
{
"should show only untrusted edge devices and other regular endpoints",
[]portainer.EndpointID{untrustedEdgeDevice.ID, regularEndpoint.ID},
EnvironmentsQuery{
edgeDevice: BoolAddr(true),
edgeDeviceUntrusted: true,
},
},
{
"should show no edge devices",
[]portainer.EndpointID{regularEndpoint.ID, regularUntrustedEdgeEndpoint.ID, regularTrustedEdgeEndpoint.ID},
EnvironmentsQuery{
edgeDevice: BoolAddr(false),
},
},
}
runTests(tests, t, handler, endpoints)
}
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
runTest(t, test, handler, endpoints)
})
}
}
func runTest(t *testing.T, test filterTest, handler *Handler, endpoints []portainer.Endpoint) {
is := assert.New(t)
filteredEndpoints, _, err := handler.filterEndpointsByQuery(endpoints, test.query, []portainer.EndpointGroup{}, &portainer.Settings{})
is.NoError(err)
is.Equal(len(test.expected), len(filteredEndpoints))
respIds := []portainer.EndpointID{}
for _, endpoint := range filteredEndpoints {
respIds = append(respIds, endpoint.ID)
}
is.ElementsMatch(test.expected, respIds)
}
func setupFilterTest(t *testing.T, endpoints []portainer.Endpoint) (handler *Handler, teardown func()) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
for _, endpoint := range endpoints {
err := store.Endpoint().Create(&endpoint)
is.NoError(err, "error creating environment")
}
err := store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
handler = NewHandler(bouncer, nil)
handler.DataStore = store
handler.ComposeStackManager = testhelpers.NewComposeStackManager()
return handler, teardown
}
+3
View File
@@ -67,6 +67,9 @@ func NewHandler(bouncer requestBouncer, demoService *demo.Service) *Handler {
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
h.Handle("/endpoints",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
h.Handle("/endpoints/agent_versions",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.agentVersions))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}",
+2 -2
View File
@@ -39,8 +39,8 @@ func (e EndpointsByGroup) Less(i, j int) bool {
return false
}
groupA := endpointGroupNames[e.endpoints[i].GroupID]
groupB := endpointGroupNames[e.endpoints[j].GroupID]
groupA := e.endpointGroupNames[e.endpoints[i].GroupID]
groupB := e.endpointGroupNames[e.endpoints[j].GroupID]
return sortorder.NaturalLess(strings.ToLower(groupA), strings.ToLower(groupB))
}
+6
View File
@@ -0,0 +1,6 @@
package endpoints
func BoolAddr(b bool) *bool {
boolVar := b
return &boolVar
}
+5 -1
View File
@@ -7,6 +7,7 @@ import (
"github.com/portainer/portainer/api/http/handler/auth"
"github.com/portainer/portainer/api/http/handler/backup"
"github.com/portainer/portainer/api/http/handler/customtemplates"
"github.com/portainer/portainer/api/http/handler/docker"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgejobs"
"github.com/portainer/portainer/api/http/handler/edgestacks"
@@ -45,6 +46,7 @@ type Handler struct {
AuthHandler *auth.Handler
BackupHandler *backup.Handler
CustomTemplatesHandler *customtemplates.Handler
DockerHandler *docker.Handler
EdgeGroupsHandler *edgegroups.Handler
EdgeJobsHandler *edgejobs.Handler
EdgeStacksHandler *edgestacks.Handler
@@ -80,7 +82,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.14.0
// @version 2.15.1
// @description.markdown api-description.md
// @termsOfService
@@ -179,6 +181,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.EndpointGroupHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/kubernetes"):
http.StripPrefix("/api", h.KubernetesHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/docker"):
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
// Helm subpath under kubernetes -> /api/endpoints/{id}/kubernetes/helm
case strings.HasPrefix(r.URL.Path, "/api/endpoints/") && strings.Contains(r.URL.Path, "/kubernetes/helm"):
+1 -2
View File
@@ -2,7 +2,6 @@ package helm
import (
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/portainer/libhelm"
@@ -108,7 +107,7 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet
hostURL := "localhost"
if !sslSettings.SelfSigned {
hostURL = strings.Split(r.Host, ":")[0]
hostURL = r.Host
}
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"net/http"
"strings"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -145,8 +144,7 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD
}
func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
hostURL := strings.Split(r.Host, ":")[0]
kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID)
kubeConfigInternal := handler.kubeClusterAccessService.GetData(r.Host, endpoint.ID)
return clientV1.NamedCluster{
Name: buildClusterName(endpoint.Name),
Cluster: clientV1.Cluster{
+24 -2
View File
@@ -30,6 +30,19 @@ type publicSettingsResponse struct {
KubeconfigExpiry string `example:"24h" default:"0"`
// Whether team sync is enabled
TeamSync bool `json:"TeamSync" example:"true"`
Edge struct {
// Whether the device has been started in edge async mode
AsyncMode bool
// The ping interval for edge agent - used in edge async mode [seconds]
PingInterval int `json:"PingInterval" example:"60"`
// The snapshot interval for edge agent - used in edge async mode [seconds]
SnapshotInterval int `json:"SnapshotInterval" example:"60"`
// The command list interval for edge agent - used in edge async mode [seconds]
CommandInterval int `json:"CommandInterval" example:"60"`
// The check in interval for edge agent (in seconds) - used in non async mode [seconds]
CheckinInterval int `example:"60"`
}
}
// @id SettingsPublic
@@ -61,6 +74,13 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
KubeconfigExpiry: appSettings.KubeconfigExpiry,
Features: appSettings.FeatureFlagSettings,
}
publicSettings.Edge.AsyncMode = appSettings.Edge.AsyncMode
publicSettings.Edge.PingInterval = appSettings.Edge.PingInterval
publicSettings.Edge.SnapshotInterval = appSettings.Edge.SnapshotInterval
publicSettings.Edge.CommandInterval = appSettings.Edge.CommandInterval
publicSettings.Edge.CheckinInterval = appSettings.EdgeAgentCheckinInterval
//if OAuth authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {
publicSettings.OAuthLogoutURI = appSettings.OAuthSettings.LogoutURI
@@ -75,8 +95,10 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
}
}
//if LDAP authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP {
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings) > 0
if publicSettings.AuthenticationMethod == portainer.AuthenticationLDAP && appSettings.LDAPSettings.GroupSearchSettings != nil {
if len(appSettings.LDAPSettings.GroupSearchSettings) > 0 {
publicSettings.TeamSync = len(appSettings.LDAPSettings.GroupSearchSettings[0].GroupBaseDN) > 0
}
}
return publicSettings
}
+17 -15
View File
@@ -124,7 +124,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
}
@@ -275,7 +275,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
}
stack.GitConfig.ConfigHash = commitID
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
}
@@ -386,7 +386,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, false)
if configErr != nil {
return configErr
}
@@ -408,14 +408,15 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
}
type composeStackDeploymentConfig struct {
stack *portainer.Stack
endpoint *portainer.Endpoint
registries []portainer.Registry
isAdmin bool
user *portainer.User
stack *portainer.Stack
endpoint *portainer.Endpoint
registries []portainer.Registry
isAdmin bool
user *portainer.User
forcePullImage bool
}
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, forcePullImage bool) (*composeStackDeploymentConfig, *httperror.HandlerError) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return nil, &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
@@ -433,11 +434,12 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
config := &composeStackDeploymentConfig{
stack: stack,
endpoint: endpoint,
registries: filteredRegistries,
isAdmin: securityContext.IsAdmin,
user: user,
stack: stack,
endpoint: endpoint,
registries: filteredRegistries,
isAdmin: securityContext.IsAdmin,
user: user,
forcePullImage: forcePullImage,
}
return config, nil
@@ -477,5 +479,5 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig,
}
}
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries, forceCreate)
return handler.StackDeployer.DeployComposeStack(config.stack, config.endpoint, config.registries, config.forcePullImage, forceCreate)
}
@@ -85,7 +85,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false, true)
if configErr != nil {
return configErr
}
@@ -226,7 +226,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
}
stack.GitConfig.ConfigHash = commitID
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false, true)
if configErr != nil {
return configErr
}
@@ -332,7 +332,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false, true)
if configErr != nil {
return configErr
}
@@ -360,9 +360,10 @@ type swarmStackDeploymentConfig struct {
prune bool
isAdmin bool
user *portainer.User
pullImage bool
}
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool, pullImage bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
@@ -386,6 +387,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
prune: prune,
isAdmin: securityContext.IsAdmin,
user: user,
pullImage: pullImage,
}
return config, nil
@@ -413,5 +415,5 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
}
}
return handler.StackDeployer.DeploySwarmStack(config.stack, config.endpoint, config.registries, config.prune)
return handler.StackDeployer.DeploySwarmStack(config.stack, config.endpoint, config.registries, config.prune, config.pullImage)
}
+16
View File
@@ -7,6 +7,8 @@ import (
"strings"
"sync"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/docker/docker/api/types"
"github.com/gorilla/mux"
"github.com/pkg/errors"
@@ -133,6 +135,20 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
}
// if stack management is disabled for non admins and the user isn't an admin, then return false. Otherwise return true
func (handler *Handler) userCanManageStacks(securityContext *security.RestrictedRequestContext, endpoint *portainer.Endpoint) (bool, error) {
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpoint.ID))
if err != nil {
return false, fmt.Errorf("Failed to get user from the database: %w", err)
}
return canCreate, nil
}
return true, nil
}
func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID) (bool, error) {
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
@@ -82,6 +82,22 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if handler.DataStore.IsErrObjectNotFound(err) {
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an environment with the specified identifier inside the database", Err: err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
}
stack.EndpointID = portainer.EndpointID(endpointID)
stack.SwarmID = swarmId
+11 -16
View File
@@ -13,7 +13,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -76,22 +75,18 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
if endpointutils.IsDockerEndpoint(endpoint) && !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve user info from request context", Err: err}
}
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err}
}
if !canCreate {
errMsg := "Stack creation is disabled for non-admin users"
return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack creation is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
+9
View File
@@ -103,6 +103,15 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack deletion is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: fmt.Errorf(errMsg)}
}
// stop scheduler updates of the stack before removal
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
+12 -2
View File
@@ -3,11 +3,12 @@ package stacks
import (
"net/http"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/errors"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -59,6 +60,15 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
if endpoint != nil {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
@@ -76,7 +86,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
}
}
+12 -3
View File
@@ -3,12 +3,12 @@ package stacks
import (
"net/http"
"github.com/portainer/portainer/api/http/errors"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -55,6 +55,15 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
if endpoint != nil {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
@@ -72,7 +81,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", errors.ErrResourceAccessDenied}
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
if resourceControl != nil {
+11 -2
View File
@@ -87,6 +87,15 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack migration is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
@@ -180,7 +189,7 @@ func (handler *Handler) migrateStack(r *http.Request, stack *portainer.Stack, ne
}
func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.Stack, next *portainer.Endpoint) *httperror.HandlerError {
config, configErr := handler.createComposeDeployConfig(r, stack, next)
config, configErr := handler.createComposeDeployConfig(r, stack, next, false)
if configErr != nil {
return configErr
}
@@ -194,7 +203,7 @@ func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.St
}
func (handler *Handler) migrateSwarmStack(r *http.Request, stack *portainer.Stack, next *portainer.Endpoint) *httperror.HandlerError {
config, configErr := handler.createSwarmDeployConfig(r, stack, next, true)
config, configErr := handler.createSwarmDeployConfig(r, stack, next, true, true)
if configErr != nil {
return configErr
}
+10 -1
View File
@@ -64,6 +64,15 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
@@ -125,7 +134,7 @@ func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.E
case portainer.DockerComposeStack:
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, false)
case portainer.DockerSwarmStack:
return handler.SwarmStackManager.Deploy(stack, true, endpoint)
return handler.SwarmStackManager.Deploy(stack, true, true, endpoint)
}
return nil
}
+9
View File
@@ -75,6 +75,15 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
if stack.Status == portainer.StackStatusInactive {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
}
+15 -2
View File
@@ -22,6 +22,8 @@ type updateComposeStackPayload struct {
StackFileContent string `example:"version: 3\n services:\n web:\n image:nginx"`
// A list of environment(endpoint) variables used during stack deployment
Env []portainer.Pair
// Force a pulling to current image with the original tag though the image is already the latest
PullImage bool `example:"false"`
}
func (payload *updateComposeStackPayload) Validate(r *http.Request) error {
@@ -38,6 +40,8 @@ type updateSwarmStackPayload struct {
Env []portainer.Pair
// Prune services that are no longer referenced (only available for Swarm stacks)
Prune bool `example:"true"`
// Force a pulling to current image with the original tag though the image is already the latest
PullImage bool `example:"false"`
}
func (payload *updateSwarmStackPayload) Validate(r *http.Request) error {
@@ -123,6 +127,15 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack editing is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
updateError := handler.updateAndDeployStack(r, stack, endpoint)
if updateError != nil {
return updateError
@@ -185,7 +198,7 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
}
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint, payload.PullImage)
if configErr != nil {
return configErr
}
@@ -222,7 +235,7 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
}
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune)
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune, payload.PullImage)
if configErr != nil {
return configErr
}
@@ -18,6 +18,7 @@ import (
type stackGitUpdatePayload struct {
AutoUpdate *portainer.StackAutoUpdate
Env []portainer.Pair
Prune bool
RepositoryReferenceName string
RepositoryAuthentication bool
RepositoryUsername string
@@ -119,6 +120,15 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack editing is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
//stop the autoupdate job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
@@ -131,6 +141,12 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
stack.UpdatedBy = user.Username
stack.UpdateDate = time.Now().Unix()
if stack.Type == portainer.DockerSwarmStack {
stack.Option = &portainer.StackOption{
Prune: payload.Prune,
}
}
if payload.RepositoryAuthentication {
password := payload.RepositoryPassword
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
@@ -24,6 +24,9 @@ type stackGitRedployPayload struct {
RepositoryUsername string
RepositoryPassword string
Env []portainer.Pair
Prune bool
// Force a pulling to current image with the original tag though the image is already the latest
PullImage bool `example:"false"`
}
func (payload *stackGitRedployPayload) Validate(r *http.Request) error {
@@ -110,6 +113,15 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
}
}
canManage, err := handler.userCanManageStacks(securityContext, endpoint)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack deletion", Err: err}
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: errMsg, Err: errors.New(errMsg)}
}
var payload stackGitRedployPayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
@@ -118,6 +130,11 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
stack.Env = payload.Env
if stack.Type == portainer.DockerSwarmStack {
stack.Option = &portainer.StackOption{
Prune: payload.Prune,
}
}
backupProjectPath := fmt.Sprintf("%s-old", stack.ProjectPath)
err = filesystem.MoveDirectory(stack.ProjectPath, backupProjectPath)
@@ -152,7 +169,7 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
}
}()
httpErr := handler.deployStack(r, stack, endpoint)
httpErr := handler.deployStack(r, stack, payload.PullImage, endpoint)
if httpErr != nil {
return httpErr
}
@@ -184,10 +201,14 @@ func (handler *Handler) stackGitRedeploy(w http.ResponseWriter, r *http.Request)
return response.JSON(w, stack)
}
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, pullImage bool, endpoint *portainer.Endpoint) *httperror.HandlerError {
switch stack.Type {
case portainer.DockerSwarmStack:
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
prune := false
if stack.Option != nil {
prune = stack.Option.Prune
}
config, httpErr := handler.createSwarmDeployConfig(r, stack, endpoint, prune, pullImage)
if httpErr != nil {
return httpErr
}
@@ -197,7 +218,7 @@ func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, end
}
case portainer.DockerComposeStack:
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint)
config, httpErr := handler.createComposeDeployConfig(r, stack, endpoint, pullImage)
if httpErr != nil {
return httpErr
}
+1 -1
View File
@@ -27,7 +27,7 @@ func NewHandler(bouncer *security.RequestBouncer, status *portainer.Status, demo
h.Handle("/status",
bouncer.PublicAccess(httperror.LoggerHandler(h.statusInspect))).Methods(http.MethodGet)
h.Handle("/status/version",
bouncer.AuthenticatedAccess(http.HandlerFunc(h.statusInspectVersion))).Methods(http.MethodGet)
bouncer.AuthenticatedAccess(http.HandlerFunc(h.version))).Methods(http.MethodGet)
return h
}
@@ -1,62 +0,0 @@
package status
import (
"encoding/json"
"net/http"
"github.com/coreos/go-semver/semver"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/libhttp/response"
)
type inspectVersionResponse struct {
// Whether portainer has an update available
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
// The latest version available
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
}
type githubData struct {
TagName string `json:"tag_name"`
}
// @id StatusInspectVersion
// @summary Check for portainer updates
// @description Check if portainer has an update available
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags status
// @produce json
// @success 200 {object} inspectVersionResponse "Success"
// @router /status/version [get]
func (handler *Handler) statusInspectVersion(w http.ResponseWriter, r *http.Request) {
motd, err := client.Get(portainer.VersionCheckURL, 5)
if err != nil {
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
return
}
var data githubData
err = json.Unmarshal(motd, &data)
if err != nil {
response.JSON(w, &inspectVersionResponse{UpdateAvailable: false})
return
}
resp := inspectVersionResponse{
UpdateAvailable: false,
}
currentVersion := semver.New(portainer.APIVersion)
latestVersion := semver.New(data.TagName)
if currentVersion.LessThan(*latestVersion) {
resp.UpdateAvailable = true
resp.LatestVersion = data.TagName
}
response.JSON(w, &resp)
}
+105
View File
@@ -0,0 +1,105 @@
package status
import (
"encoding/json"
"net/http"
"strconv"
"github.com/coreos/go-semver/semver"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/build"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/libhttp/response"
log "github.com/sirupsen/logrus"
)
type versionResponse struct {
// Whether portainer has an update available
UpdateAvailable bool `json:"UpdateAvailable" example:"false"`
// The latest version available
LatestVersion string `json:"LatestVersion" example:"2.0.0"`
ServerVersion string
DatabaseVersion string
Build BuildInfo
}
type BuildInfo struct {
BuildNumber string
ImageTag string
NodejsVersion string
YarnVersion string
WebpackVersion string
GoVersion string
}
// @id Version
// @summary Check for portainer updates
// @description Check if portainer has an update available
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags status
// @produce json
// @success 200 {object} versionResponse "Success"
// @router /status/version [get]
func (handler *Handler) version(w http.ResponseWriter, r *http.Request) {
result := &versionResponse{
ServerVersion: portainer.APIVersion,
DatabaseVersion: strconv.Itoa(portainer.DBVersion),
Build: BuildInfo{
BuildNumber: build.BuildNumber,
ImageTag: build.ImageTag,
NodejsVersion: build.NodejsVersion,
YarnVersion: build.YarnVersion,
WebpackVersion: build.WebpackVersion,
GoVersion: build.GoVersion,
},
}
latestVersion := getLatestVersion()
if hasNewerVersion(portainer.APIVersion, latestVersion) {
result.UpdateAvailable = true
result.LatestVersion = latestVersion
}
response.JSON(w, &result)
}
func getLatestVersion() string {
motd, err := client.Get(portainer.VersionCheckURL, 5)
if err != nil {
log.WithError(err).Debug("couldn't fetch latest Portainer release version")
return ""
}
var data struct {
TagName string `json:"tag_name"`
}
err = json.Unmarshal(motd, &data)
if err != nil {
log.WithError(err).Debug("couldn't parse latest Portainer version")
return ""
}
return data.TagName
}
func hasNewerVersion(currentVersion, latestVersion string) bool {
currentVersionSemver, err := semver.NewVersion(currentVersion)
if err != nil {
log.WithField("version", currentVersion).Debug("current Portainer version isn't a semver")
return false
}
latestVersionSemver, err := semver.NewVersion(latestVersion)
if err != nil {
log.WithField("version", latestVersion).Debug("latest Portainer version isn't a semver")
return false
}
return currentVersionSemver.LessThan(*latestVersionSemver)
}
+4 -1
View File
@@ -24,11 +24,14 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
adminRouter := h.NewRoute().Subrouter()
adminRouter.Use(bouncer.AdminAccess)
restrictedRouter := h.NewRoute().Subrouter()
restrictedRouter.Use(bouncer.RestrictedAccess)
teamLeaderRouter := h.NewRoute().Subrouter()
teamLeaderRouter.Use(bouncer.TeamLeaderAccess)
adminRouter.Handle("/teams", httperror.LoggerHandler(h.teamCreate)).Methods(http.MethodPost)
teamLeaderRouter.Handle("/teams", httperror.LoggerHandler(h.teamList)).Methods(http.MethodGet)
restrictedRouter.Handle("/teams", httperror.LoggerHandler(h.teamList)).Methods(http.MethodGet)
teamLeaderRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamInspect)).Methods(http.MethodGet)
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamUpdate)).Methods(http.MethodPut)
adminRouter.Handle("/teams/{id}", httperror.LoggerHandler(h.teamDelete)).Methods(http.MethodDelete)
@@ -82,7 +82,7 @@ func (handler *Handler) userUpdatePassword(w http.ResponseWriter, r *http.Reques
err = handler.CryptoService.CompareHashAndData(user.Password, payload.Password)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Specified password do not match actual password", httperrors.ErrUnauthorized}
return &httperror.HandlerError{http.StatusForbidden, "Current password doesn't match", errors.New("Current password does not match the password provided. Please try again")}
}
if !handler.passwordStrengthChecker.Check(payload.NewPassword) {
+2 -15
View File
@@ -5,14 +5,13 @@ import (
"log"
"net"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/agent"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/url"
)
// ProxyServer provide an extended proxy with a local server to forward requests
@@ -34,7 +33,7 @@ func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*Proxy
urlString = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := parseURL(urlString)
endpointURL, err := url.ParseURL(urlString)
if err != nil {
return nil, errors.Wrapf(err, "failed parsing url %s", endpoint.URL)
}
@@ -99,15 +98,3 @@ func (proxy *ProxyServer) Close() {
proxy.server.Close()
}
}
// parseURL parses the endpointURL using url.Parse.
//
// to prevent an error when url has port but no protocol prefix
// we add `//` prefix if needed
func parseURL(endpointURL string) (*url.URL, error) {
if !strings.HasPrefix(endpointURL, "http") && !strings.HasPrefix(endpointURL, "tcp") && !strings.HasPrefix(endpointURL, "//") {
endpointURL = fmt.Sprintf("//%s", endpointURL)
}
return url.Parse(endpointURL)
}
+3 -3
View File
@@ -5,13 +5,13 @@ import (
"io"
"log"
"net/http"
"net/url"
"strings"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/docker"
"github.com/portainer/portainer/api/internal/url"
)
func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
@@ -23,7 +23,7 @@ func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.
}
func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
endpointURL, err := url.Parse(endpoint.URL)
endpointURL, err := url.ParseURL(endpoint.URL)
if err != nil {
return nil, err
}
@@ -38,7 +38,7 @@ func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (h
rawURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := url.Parse(rawURL)
endpointURL, err := url.ParseURL(rawURL)
if err != nil {
return nil, err
}
+1 -1
View File
@@ -23,7 +23,7 @@ type (
}
portainerRegistryAuthenticationHeader struct {
RegistryId portainer.RegistryID `json:"registryId"`
RegistryId *portainer.RegistryID `json:"registryId"`
}
)
+14 -1
View File
@@ -446,7 +446,20 @@ func (transport *Transport) decorateRegistryAuthenticationHeader(request *http.R
return err
}
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, originalHeaderData.RegistryId, accessContext)
// delete header and exist function without error if Front End
// passes empty json. This is to restore original behavior which
// never originally passed this header
if string(decodedHeaderData) == "{}" {
request.Header.Del("X-Registry-Auth")
return nil
}
// only set X-Registry-Auth if registryId is defined
if originalHeaderData.RegistryId == nil {
return nil
}
authenticationHeader, err := createRegistryAuthenticationHeader(transport.dataStore, *originalHeaderData.RegistryId, accessContext)
if err != nil {
return err
}
+4 -4
View File
@@ -81,11 +81,11 @@ func FilterRegistries(registries []portainer.Registry, user *portainer.User, tea
}
// FilterEndpoints filters environments(endpoints) based on user role and team memberships.
// Non administrator and non-team-leader only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
// Non administrator only have access to authorized environments(endpoints) (can be inherited via endpoint groups).
func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.Endpoint {
filteredEndpoints := endpoints
if !context.IsAdmin && !context.IsTeamLeader {
if !context.IsAdmin {
filteredEndpoints = make([]portainer.Endpoint, 0)
for _, endpoint := range endpoints {
@@ -101,11 +101,11 @@ func FilterEndpoints(endpoints []portainer.Endpoint, groups []portainer.Endpoint
}
// FilterEndpointGroups filters environment(endpoint) groups based on user role and team memberships.
// Non administrator users and Non-team-leaders only have access to authorized environment(endpoint) groups.
// Non administrator users only have access to authorized environment(endpoint) groups.
func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *RestrictedRequestContext) []portainer.EndpointGroup {
filteredEndpointGroups := endpointGroups
if !context.IsAdmin && !context.IsTeamLeader {
if !context.IsAdmin {
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
for _, group := range endpointGroups {
+4
View File
@@ -21,6 +21,7 @@ import (
"github.com/portainer/portainer/api/http/handler/auth"
"github.com/portainer/portainer/api/http/handler/backup"
"github.com/portainer/portainer/api/http/handler/customtemplates"
dockerhandler "github.com/portainer/portainer/api/http/handler/docker"
"github.com/portainer/portainer/api/http/handler/edgegroups"
"github.com/portainer/portainer/api/http/handler/edgejobs"
"github.com/portainer/portainer/api/http/handler/edgestacks"
@@ -184,6 +185,8 @@ func (server *Server) Start() error {
var kubernetesHandler = kubehandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.JWTService, server.KubeClusterAccessService, server.KubernetesClientFactory)
var dockerHandler = dockerhandler.NewHandler(requestBouncer, server.AuthorizationService, server.DataStore, server.DockerClientFactory)
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"), adminMonitor.WasInstanceDisabled)
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.JWTService, server.KubernetesDeployer, server.HelmPackageManager, server.KubeClusterAccessService)
@@ -275,6 +278,7 @@ func (server *Server) Start() error {
AuthHandler: authHandler,
BackupHandler: backupHandler,
CustomTemplatesHandler: customTemplatesHandler,
DockerHandler: dockerHandler,
EdgeGroupsHandler: edgeGroupsHandler,
EdgeJobsHandler: edgeJobsHandler,
EdgeStacksHandler: edgeStacksHandler,
+1 -1
View File
@@ -37,7 +37,7 @@ func parseRegToken(registry *portainer.Registry) (username, password string, err
func EnsureRegTokenValid(dataStore dataservices.DataStore, registry *portainer.Registry) (err error) {
if registry.Type == portainer.EcrRegistry {
if isRegTokenValid(registry) {
log.Println("[DEBUG] [registry, GetEcrAccessToken] [message: curretn ECR token is still valid]")
log.Println("[DEBUG] [registry, GetEcrAccessToken] [message: current ECR token is still valid]")
} else {
err = doGetRegToken(dataStore, registry)
if err != nil {
+40
View File
@@ -0,0 +1,40 @@
package set
type SetKey interface {
~int | ~string
}
type Set[T SetKey] map[T]bool
func (s Set[T]) Add(key T) {
s[key] = true
}
func (s Set[T]) Contains(key T) bool {
_, ok := s[key]
return ok
}
func (s Set[T]) Remove(key T) {
delete(s, key)
}
func (s Set[T]) Len() int {
return len(s)
}
func (s Set[T]) IsEmpty() bool {
return len(s) == 0
}
func (s Set[T]) Keys() []T {
keys := make([]T, s.Len())
i := 0
for k := range s {
keys[i] = k
i++
}
return keys
}
+22
View File
@@ -2,11 +2,14 @@ package snapshot
import (
"context"
"crypto/tls"
"errors"
"log"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/agent"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/dataservices"
)
@@ -87,6 +90,24 @@ func SupportDirectSnapshot(endpoint *portainer.Endpoint) bool {
// SnapshotEndpoint will create a snapshot of the environment(endpoint) based on the environment(endpoint) type.
// If the snapshot is a success, it will be associated to the environment(endpoint).
func (service *Service) SnapshotEndpoint(endpoint *portainer.Endpoint) error {
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment {
var err error
var tlsConfig *tls.Config
if endpoint.TLSConfig.TLS {
tlsConfig, err = crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return err
}
}
_, version, err := agent.GetAgentVersionAndPlatform(endpoint.URL, tlsConfig)
if err != nil {
return err
}
endpoint.Agent.Version = version
}
switch endpoint.Type {
case portainer.AzureEnvironment:
return nil
@@ -175,6 +196,7 @@ func (service *Service) snapshotEndpoints() error {
latestEndpointReference.Snapshots = endpoint.Snapshots
latestEndpointReference.Kubernetes.Snapshots = endpoint.Kubernetes.Snapshots
latestEndpointReference.Agent.Version = endpoint.Agent.Version
err = service.dataStore.Endpoint().UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference)
if err != nil {
+24
View File
@@ -0,0 +1,24 @@
package url
import (
"fmt"
"net/url"
"strings"
)
// ParseURL parses the endpointURL using url.Parse.
//
// to prevent an error when url has port but no protocol prefix
// we add `//` prefix if needed
func ParseURL(endpointURL string) (*url.URL, error) {
if !strings.HasPrefix(endpointURL, "http") &&
!strings.HasPrefix(endpointURL, "tcp") &&
!strings.HasPrefix(endpointURL, "//") &&
!strings.HasPrefix(endpointURL, `unix:`) &&
!strings.HasPrefix(endpointURL, `npipe:`) {
endpointURL = fmt.Sprintf("//%s", endpointURL)
}
return url.Parse(endpointURL)
}
+11 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/sirupsen/logrus"
)
// KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data
@@ -94,11 +95,20 @@ func (service *kubeClusterAccessService) IsSecure() bool {
// - pass down params to binaries
func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData {
baseURL := service.baseURL
// When the api call is internal, the baseURL should not be used.
if hostURL == "localhost" {
hostURL = hostURL + service.httpsBindAddr
baseURL = "/"
}
if baseURL != "/" {
baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/"))
}
clusterURL := hostURL + service.httpsBindAddr + baseURL
logrus.Infof("[kubeconfig] [hostURL: %s, httpsBindAddr: %s, baseURL: %s]", hostURL, service.httpsBindAddr, baseURL)
clusterURL := hostURL + baseURL
clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID)
@@ -115,7 +115,7 @@ func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) {
clusterAccessDetails := kcs.GetData("mysite.com", 1)
wantClusterAccessDetails := kubernetesClusterAccessData{
ClusterServerURL: "https://mysite.com:9443/api/endpoints/1/kubernetes",
ClusterServerURL: "https://mysite.com/api/endpoints/1/kubernetes",
CertificateAuthorityFile: "",
CertificateAuthorityData: "",
}
+74 -37
View File
@@ -3,16 +3,18 @@ package oauth
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"mime"
"net/http"
"net/url"
"strings"
"golang.org/x/oauth2"
"github.com/golang-jwt/jwt"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
log "github.com/sirupsen/logrus"
)
// Service represents a service used to authenticate users against an authorization server
@@ -29,17 +31,39 @@ func NewService() *Service {
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) {
token, err := getOAuthToken(code, configuration)
if err != nil {
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
log.Debugf("[internal,oauth] [message: failed retrieving oauth token: %v]", err)
return "", err
}
username, err := getUsername(token.AccessToken, configuration)
idToken, err := getIdToken(token)
if err != nil {
log.Printf("[DEBUG] - Failed retrieving oauth user name: %v", err)
log.Debugf("[internal,oauth] [message: failed parsing id_token: %v]", err)
}
resource, err := getResource(token.AccessToken, configuration)
if err != nil {
log.Debugf("[internal,oauth] [message: failed retrieving resource: %v]", err)
return "", err
}
resource = mergeSecondIntoFirst(idToken, resource)
username, err := getUsername(resource, configuration)
if err != nil {
log.Debugf("[internal,oauth] [message: failed retrieving username: %v]", err)
return "", err
}
return username, nil
}
// mergeSecondIntoFirst merges the overlap map into the base overwriting any existing values.
func mergeSecondIntoFirst(base map[string]interface{}, overlap map[string]interface{}) map[string]interface{} {
for k, v := range overlap {
base[k] = v
}
return base
}
func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) {
unescapedCode, err := url.QueryUnescape(code)
if err != nil {
@@ -55,27 +79,55 @@ func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2
return token, nil
}
func getUsername(token string, configuration *portainer.OAuthSettings) (string, error) {
// getIdToken retrieves parsed id_token from the OAuth token response.
// This is necessary for OAuth providers like Azure
// that do not provide information about user groups on the user resource endpoint.
func getIdToken(token *oauth2.Token) (map[string]interface{}, error) {
tokenData := make(map[string]interface{})
idToken := token.Extra("id_token")
if idToken == nil {
return tokenData, nil
}
jwtParser := jwt.Parser{
SkipClaimsValidation: true,
}
t, _, err := jwtParser.ParseUnverified(idToken.(string), jwt.MapClaims{})
if err != nil {
return tokenData, errors.Wrap(err, "failed to parse id_token")
}
if claims, ok := t.Claims.(jwt.MapClaims); ok {
for k, v := range claims {
tokenData[k] = v
}
}
return tokenData, nil
}
func getResource(token string, configuration *portainer.OAuthSettings) (map[string]interface{}, error) {
req, err := http.NewRequest("GET", configuration.ResourceURI, nil)
if err != nil {
return "", err
return nil, err
}
client := &http.Client{}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return "", err
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
return nil, err
}
if resp.StatusCode != http.StatusOK {
return "", &oauth2.RetrieveError{
return nil, &oauth2.RetrieveError{
Response: resp,
Body: body,
}
@@ -83,47 +135,32 @@ func getUsername(token string, configuration *portainer.OAuthSettings) (string,
content, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if err != nil {
return "", err
return nil, err
}
if content == "application/x-www-form-urlencoded" || content == "text/plain" {
values, err := url.ParseQuery(string(body))
if err != nil {
return "", err
return nil, err
}
username := values.Get(configuration.UserIdentifier)
if username == "" {
return username, &oauth2.RetrieveError{
Response: resp,
Body: body,
datamap := make(map[string]interface{})
for k, v := range values {
if len(v) == 0 {
datamap[k] = ""
} else {
datamap[k] = v[0]
}
}
return username, nil
return datamap, nil
}
var datamap map[string]interface{}
if err = json.Unmarshal(body, &datamap); err != nil {
return "", err
return nil, err
}
username, ok := datamap[configuration.UserIdentifier].(string)
if ok && username != "" {
return username, nil
}
if !ok {
username, ok := datamap[configuration.UserIdentifier].(float64)
if ok && username != 0 {
return fmt.Sprint(int(username)), nil
}
}
return "", &oauth2.RetrieveError{
Response: resp,
Body: body,
}
return datamap, nil
}
func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
@@ -137,6 +174,6 @@ func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
ClientSecret: configuration.ClientSecret,
Endpoint: endpoint,
RedirectURL: configuration.RedirectURI,
Scopes: []string{configuration.Scopes},
Scopes: strings.Split(configuration.Scopes, ","),
}
}
+24
View File
@@ -0,0 +1,24 @@
package oauth
import (
"errors"
"fmt"
portainer "github.com/portainer/portainer/api"
)
func getUsername(datamap map[string]interface{}, configuration *portainer.OAuthSettings) (string, error) {
username, ok := datamap[configuration.UserIdentifier].(string)
if ok && username != "" {
return username, nil
}
if !ok {
username, ok := datamap[configuration.UserIdentifier].(float64)
if ok && username != 0 {
return fmt.Sprint(int(username)), nil
}
}
return "", errors.New("failed to extract username from oauth resource")
}
+80
View File
@@ -0,0 +1,80 @@
package oauth
import (
"testing"
portaineree "github.com/portainer/portainer/api"
)
func Test_getUsername(t *testing.T) {
t.Run("fails for non-matching user identifier", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"name": "john"}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if user identifier doesn't exist as key in oauth userinfo object")
}
})
t.Run("fails if username is empty string", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": ""}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if username from oauth userinfo object is empty string")
}
})
t.Run("fails if username is 0 int", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": 0}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if username from oauth userinfo object is 0 val int")
}
})
t.Run("fails if username is negative int", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": -1}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if username from oauth userinfo object is -1 (negative) int")
}
})
t.Run("succeeds if username is matched and is not empty", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": "john"}
_, err := getUsername(datamap, oauthSettings)
if err != nil {
t.Errorf("getUsername should succeed if username from oauth userinfo object matched and non-empty")
}
})
// looks like a bug!?
t.Run("fails if username is matched and is positive int", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": 1}
_, err := getUsername(datamap, oauthSettings)
if err == nil {
t.Errorf("getUsername should fail if username from oauth userinfo object matched is positive int")
}
})
t.Run("succeeds if username is matched and is non-zero (or negative) float", func(t *testing.T) {
oauthSettings := &portaineree.OAuthSettings{UserIdentifier: "username"}
datamap := map[string]interface{}{"username": 1.1}
_, err := getUsername(datamap, oauthSettings)
if err != nil {
t.Errorf("getUsername should succeed if username from oauth userinfo object matched and non-zero (or negative)")
}
})
}
+145
View File
@@ -0,0 +1,145 @@
package oauth
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/oauth/oauthtest"
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)
func Test_getOAuthToken(t *testing.T) {
validCode := "valid-code"
srv, config := oauthtest.RunOAuthServer(validCode, &portainer.OAuthSettings{})
defer srv.Close()
t.Run("getOAuthToken fails upon invalid code", func(t *testing.T) {
code := ""
_, err := getOAuthToken(code, config)
if err == nil {
t.Errorf("getOAuthToken should fail upon providing invalid code; code=%v", code)
}
})
t.Run("getOAuthToken succeeds upon providing valid code", func(t *testing.T) {
code := validCode
token, err := getOAuthToken(code, config)
if token == nil || err != nil {
t.Errorf("getOAuthToken should successfully return access token upon providing valid code")
}
})
}
func Test_getIdToken(t *testing.T) {
verifiedToken := `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTM1NDA3MjksImV4cCI6MTY4NTA3NjcyOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2huIiwiU3VybmFtZSI6IkRvZSIsIkdyb3VwcyI6WyJGaXJzdCIsIlNlY29uZCJdfQ.GeU8XCV4Y4p5Vm-i63Aj7UP5zpb_0Zxb7-DjM2_z-s8`
nonVerifiedToken := `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTM1NDA3MjksImV4cCI6MTY4NTA3NjcyOSwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoiam9obi5kb2VAZXhhbXBsZS5jb20iLCJHaXZlbk5hbWUiOiJKb2huIiwiU3VybmFtZSI6IkRvZSIsIkdyb3VwcyI6WyJGaXJzdCIsIlNlY29uZCJdfQ.`
claims := map[string]interface{}{
"iss": "Online JWT Builder",
"iat": float64(1653540729),
"exp": float64(1685076729),
"aud": "www.example.com",
"sub": "john.doe@example.com",
"GivenName": "John",
"Surname": "Doe",
"Groups": []interface{}{"First", "Second"},
}
tests := []struct {
testName string
idToken string
expectedResult map[string]interface{}
expectedError error
}{
{
testName: "should return claims if token exists and is verified",
idToken: verifiedToken,
expectedResult: claims,
expectedError: nil,
},
{
testName: "should return claims if token exists but is not verified",
idToken: nonVerifiedToken,
expectedResult: claims,
expectedError: nil,
},
{
testName: "should return empty map if token does not exist",
idToken: "",
expectedResult: make(map[string]interface{}),
expectedError: nil,
},
}
for _, tc := range tests {
t.Run(tc.testName, func(t *testing.T) {
token := &oauth2.Token{}
if tc.idToken != "" {
token = token.WithExtra(map[string]interface{}{"id_token": tc.idToken})
}
result, err := getIdToken(token)
assert.Equal(t, err, tc.expectedError)
assert.Equal(t, result, tc.expectedResult)
})
}
}
func Test_getResource(t *testing.T) {
srv, config := oauthtest.RunOAuthServer("", &portainer.OAuthSettings{})
defer srv.Close()
t.Run("should fail upon missing Authorization Bearer header", func(t *testing.T) {
_, err := getResource("", config)
if err == nil {
t.Errorf("getResource should fail if access token is not provided in auth bearer header")
}
})
t.Run("should fail upon providing incorrect Authorization Bearer header", func(t *testing.T) {
_, err := getResource("incorrect-token", config)
if err == nil {
t.Errorf("getResource should fail if incorrect access token provided in auth bearer header")
}
})
t.Run("should succeed upon providing correct Authorization Bearer header", func(t *testing.T) {
_, err := getResource(oauthtest.AccessToken, config)
if err != nil {
t.Errorf("getResource should succeed if correct access token provided in auth bearer header")
}
})
}
func Test_Authenticate(t *testing.T) {
code := "valid-code"
authService := NewService()
t.Run("should fail if user identifier does not get matched in resource", func(t *testing.T) {
srv, config := oauthtest.RunOAuthServer(code, &portainer.OAuthSettings{})
defer srv.Close()
_, err := authService.Authenticate(code, config)
if err == nil {
t.Error("Authenticate should fail to extract username from resource if incorrect UserIdentifier provided")
}
})
t.Run("should succeed if user identifier does get matched in resource", func(t *testing.T) {
config := &portainer.OAuthSettings{UserIdentifier: "username"}
srv, config := oauthtest.RunOAuthServer(code, config)
defer srv.Close()
username, err := authService.Authenticate(code, config)
if err != nil {
t.Errorf("Authenticate should succeed to extract username from resource if correct UserIdentifier provided; UserIdentifier=%s", config.UserIdentifier)
}
want := "test-oauth-user"
if username != want {
t.Errorf("Authenticate should return correct username; got=%s, want=%s", username, want)
}
})
}
+96
View File
@@ -0,0 +1,96 @@
package oauthtest
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/gorilla/mux"
portainer "github.com/portainer/portainer/api"
)
const (
AccessToken = "test-token"
)
// OAuthRoutes is an OAuth 2.0 compliant handler
func OAuthRoutes(code string, config *portainer.OAuthSettings) http.Handler {
router := mux.NewRouter()
router.HandleFunc(
"/authorize",
func(w http.ResponseWriter, req *http.Request) {
location := fmt.Sprintf("%s?code=%s&state=%s", config.RedirectURI, code, "anything")
// w.Header().Set("Location", location)
// w.WriteHeader(http.StatusFound)
http.Redirect(w, req, location, http.StatusFound)
},
).Methods(http.MethodGet)
router.HandleFunc(
"/access_token",
func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
if err := req.ParseForm(); err != nil {
fmt.Fprintf(w, "ParseForm() err: %v", err)
return
}
reqCode := req.FormValue("code")
if reqCode != code {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"token_type": "Bearer",
"expires_in": 86400,
"access_token": AccessToken,
"scope": "groups",
})
},
).Methods(http.MethodPost)
router.HandleFunc(
"/user",
func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
authHeader := req.Header.Get("Authorization")
splitToken := strings.Split(authHeader, "Bearer ")
if len(splitToken) < 2 || splitToken[1] != AccessToken {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]interface{}{
"username": "test-oauth-user",
"groups": "testing",
})
},
).Methods(http.MethodGet)
return router
}
// RunOAuthServer is a barebones OAuth 2.0 compliant test server which can be used to test OAuth 2 functionality
func RunOAuthServer(code string, config *portainer.OAuthSettings) (*httptest.Server, *portainer.OAuthSettings) {
srv := httptest.NewUnstartedServer(http.DefaultServeMux)
addr := srv.Listener.Addr()
config.AuthorizationURI = fmt.Sprintf("http://%s/authorize", addr)
config.AccessTokenURI = fmt.Sprintf("http://%s/access_token", addr)
config.ResourceURI = fmt.Sprintf("http://%s/user", addr)
config.RedirectURI = fmt.Sprintf("http://%s/", addr)
srv.Config.Handler = OAuthRoutes(code, config)
srv.Start()
return srv, config
}
+41 -3
View File
@@ -199,6 +199,8 @@ type (
StackCount int `json:"StackCount"`
SnapshotRaw DockerSnapshotRaw `json:"DockerSnapshotRaw"`
NodeCount int `json:"NodeCount"`
GpuUseAll bool `json:"GpuUseAll"`
GpuUseList []string `json:"GpuUseList"`
}
// DockerSnapshotRaw represents all the information related to a snapshot as returned by the Docker API
@@ -310,6 +312,7 @@ type (
GroupID EndpointGroupID `json:"GroupId" example:"1"`
// URL or IP address where exposed containers will be reachable
PublicURL string `json:"PublicURL" example:"docker.mydomain.tld:2375"`
Gpus []Pair `json:"Gpus"`
TLSConfig TLSConfiguration `json:"TLSConfig"`
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty" example:""`
// List of tag identifiers to which this environment(endpoint) is associated
@@ -345,6 +348,21 @@ type (
// Whether the device has been trusted or not by the user
UserTrusted bool
Edge struct {
// Whether the device has been started in edge async mode
AsyncMode bool
// The ping interval for edge agent - used in edge async mode [seconds]
PingInterval int `json:"PingInterval" example:"60"`
// The snapshot interval for edge agent - used in edge async mode [seconds]
SnapshotInterval int `json:"SnapshotInterval" example:"60"`
// The command list interval for edge agent - used in edge async mode [seconds]
CommandInterval int `json:"CommandInterval" example:"60"`
}
Agent struct {
Version string `example:"1.0.0"`
}
// Deprecated fields
// Deprecated in DBVersion == 4
TLS bool `json:"TLS,omitempty"`
@@ -837,6 +855,17 @@ type (
// EdgePortainerURL is the URL that is exposed to edge agents
EdgePortainerURL string `json:"EdgePortainerUrl"`
Edge struct {
// The command list interval for edge agent - used in edge async mode (in seconds)
CommandInterval int `json:"CommandInterval" example:"5"`
// The ping interval for edge agent - used in edge async mode (in seconds)
PingInterval int `json:"PingInterval" example:"5"`
// The snapshot interval for edge agent - used in edge async mode (in seconds)
SnapshotInterval int `json:"SnapshotInterval" example:"5"`
// EdgeAsyncMode enables edge async mode by default
AsyncMode bool
}
// Deprecated fields
DisplayDonationHeader bool
DisplayExternalContributors bool
@@ -900,6 +929,8 @@ type (
AdditionalFiles []string `json:"AdditionalFiles"`
// The auto update settings of a git stack
AutoUpdate *StackAutoUpdate `json:"AutoUpdate"`
// The stack deployment option
Option *StackOption `json:"Option"`
// The git config of this stack
GitConfig *gittypes.RepoConfig
// Whether the stack is from a app template
@@ -920,6 +951,12 @@ type (
JobID string `example:"15"`
}
// StackOption represents the options for stack deployment
StackOption struct {
// Prune services that are no longer referenced
Prune bool `example:"false"`
}
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
StackID int
@@ -1206,6 +1243,7 @@ type (
NormalizeStackName(name string) string
Up(ctx context.Context, stack *Stack, endpoint *Endpoint, forceRereate bool) error
Down(ctx context.Context, stack *Stack, endpoint *Endpoint) error
Pull(ctx context.Context, stack *Stack, endpoint *Endpoint) error
}
// CryptoService represents a service for encrypting/hashing data
@@ -1355,7 +1393,7 @@ type (
SwarmStackManager interface {
Login(registries []Registry, endpoint *Endpoint) error
Logout(endpoint *Endpoint) error
Deploy(stack *Stack, prune bool, endpoint *Endpoint) error
Deploy(stack *Stack, prune bool, pullImage bool, endpoint *Endpoint) error
Remove(stack *Stack, endpoint *Endpoint) error
NormalizeStackName(name string) string
}
@@ -1363,9 +1401,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.14.0"
APIVersion = "2.15.1"
// DBVersion is the version number of the Portainer database
DBVersion = 50
DBVersion = 61
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server
+2 -2
View File
@@ -89,12 +89,12 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
switch stack.Type {
case portainer.DockerComposeStack:
err := deployer.DeployComposeStack(stack, endpoint, registries, false)
err := deployer.DeployComposeStack(stack, endpoint, registries, true, false)
if err != nil {
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
}
case portainer.DockerSwarmStack:
err := deployer.DeploySwarmStack(stack, endpoint, registries, true)
err := deployer.DeploySwarmStack(stack, endpoint, registries, true, true)
if err != nil {
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
}
+2 -2
View File
@@ -28,11 +28,11 @@ func (g *gitService) LatestCommitID(repositoryURL, referenceName, username, pass
type noopDeployer struct{}
func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error {
func (s *noopDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
return nil
}
func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forceRereate bool) error {
func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRereate bool) error {
return nil
}
+18 -6
View File
@@ -13,8 +13,8 @@ import (
)
type StackDeployer interface {
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forceRereate bool) error
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRereate bool) error
DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error
}
@@ -35,24 +35,36 @@ func NewStackDeployer(swarmStackManager portainer.SwarmStackManager, composeStac
}
}
func (d *stackDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error {
func (d *stackDeployer) DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool, pullImage bool) error {
d.lock.Lock()
defer d.lock.Unlock()
d.swarmStackManager.Login(registries, endpoint)
defer d.swarmStackManager.Logout(endpoint)
return d.swarmStackManager.Deploy(stack, prune, endpoint)
return d.swarmStackManager.Deploy(stack, prune, pullImage, endpoint)
}
func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forceRereate bool) error {
func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, forcePullImage bool, forceRereate bool) error {
d.lock.Lock()
defer d.lock.Unlock()
d.swarmStackManager.Login(registries, endpoint)
defer d.swarmStackManager.Logout(endpoint)
return d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate)
// --force-recreate doesn't pull updated images
if forcePullImage {
err := d.composeStackManager.Pull(context.TODO(), stack, endpoint)
if err != nil {
return err
}
}
err := d.composeStackManager.Up(context.TODO(), stack, endpoint, forceRereate)
if err != nil {
d.composeStackManager.Down(context.TODO(), stack, endpoint)
}
return err
}
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
+11
View File
@@ -693,6 +693,12 @@ definitions:
$ref: '#/definitions/portainer.DockerSnapshotRaw'
DockerVersion:
type: string
GpuUseAll:
type: boolean
GpuUseList:
items:
type: string
type: array
HealthyContainerCount:
type: integer
ImageCount:
@@ -849,6 +855,11 @@ definitions:
EdgeKey:
description: The key which is used to map the agent to Portainer
type: string
Gpus:
description: Endpoint Gpus information
items:
$ref: '#/definitions/portainer.Pair'
type: array
GroupId:
description: Endpoint group identifier
example: 1
+2
View File
@@ -0,0 +1,2 @@
export default 'SvgrURL';
export const ReactComponent = 'div';
@@ -1,3 +1,4 @@
<button type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn ng-scope" button-spinner="$ctrl.state.uploadInProgress">
<i style="margin: 0" class="fa fa-upload" ng-if="!$ctrl.state.uploadInProgress"></i>
<button ng-if="!$ctrl.state.uploadInProgress" type="button" ngf-select="$ctrl.onFileSelected($file)" class="btn btn-light ng-scope">
<pr-icon icon="'upload'" feather="true"></pr-icon>
</button>
<button ng-if="$ctrl.state.uploadInProgress" type="button" class="btn btn-sm btn-light" button-spinner="$ctrl.state.uploadInProgress"></button>
@@ -1,45 +1,57 @@
<div class="datatable">
<rd-widget>
<rd-widget-header icon="{{ $ctrl.titleIcon }}" title-text="{{ $ctrl.titleText }}">
<file-uploader authorization="DockerAgentBrowsePut" ng-if="$ctrl.isUploadAllowed" on-file-selected="($ctrl.onFileSelectedForUpload)"> </file-uploader>
</rd-widget-header>
<rd-widget-body classes="no-padding">
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-model-options="{ debounce: 300 }"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
/>
<div class="toolBar">
<div class="toolBarTitle vertical-center">
<div class="widget-icon space-right">
<pr-icon icon="$ctrl.titleIcon" feather="true"></pr-icon>
</div>
{{ $ctrl.titleText }}
</div>
<div class="searchBar vertical-center">
<pr-icon icon="'search'" feather="true" class-name="'searchIcon'"></pr-icon>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-model-options="{ debounce: 300 }"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
/>
</div>
<file-uploader authorization="DockerAgentBrowsePut" ng-if="$ctrl.isUploadAllowed" on-file-selected="($ctrl.onFileSelectedForUpload)"> </file-uploader>
</div>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('Name')">
Name
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
</a>
<table-column-header
col-title="'Name'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Name'"
is-sorted-desc="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Name')"
></table-column-header>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Size')">
Size
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Size' && $ctrl.state.reverseOrder"></i>
</a>
<table-column-header
col-title="'Size'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Size'"
is-sorted-desc="$ctrl.state.orderBy === 'Size' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Size')"
></table-column-header>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('ModTime')">
Last modification
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ModTime' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'ModTime' && $ctrl.state.reverseOrder"></i>
</a>
<table-column-header
col-title="'Last modification'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'ModTime'"
is-sorted-desc="$ctrl.state.orderBy === 'ModTime' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('ModTime')"
></table-column-header>
</th>
<th> Actions </th>
</tr>
@@ -47,12 +59,12 @@
<tbody>
<tr ng-if="!$ctrl.isRoot">
<td colspan="4">
<a ng-click="$ctrl.goToParent()"><i class="fa fa-level-up-alt space-right"></i>Go to parent</a>
<a ng-click="$ctrl.goToParent()"><pr-icon icon="'corner-left-up'" feather="true"></pr-icon>Go to parent</a>
</td>
</tr>
<tr ng-repeat="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder))">
<td>
<span ng-if="item.edit">
<span ng-if="item.edit" class="vertical-center">
<input
class="input-sm"
type="text"
@@ -60,27 +72,27 @@
on-enter-key="$ctrl.rename({ name: item.Name, newName: item.newName }); item.edit = false"
auto-focus
/>
<a class="interactive" ng-click="item.edit = false;"><i class="fa fa-times"></i></a>
<a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"><i class="fa fa-check-square"></i></a>
<a class="interactive" ng-click="item.edit = false;"><pr-icon icon="'x'" feather="true"></pr-icon></a>
<a class="interactive" ng-click="$ctrl.rename({name: item.Name, newName: item.newName}); item.edit = false;"><pr-icon icon="'check'" feather="true"></pr-icon></a>
</span>
<span ng-if="!item.edit && item.Dir">
<a ng-click="$ctrl.browse({name: item.Name})"><i class="fa fa-folder space-right" aria-hidden="true"></i>{{ item.Name }}</a>
<a ng-click="$ctrl.browse({name: item.Name})" class="vertical-center"><pr-icon icon="'folder'" feather="true"></pr-icon>{{ item.Name }}</a>
</span>
<span ng-if="!item.edit && !item.Dir"> <i class="fa fa-file space-right" aria-hidden="true"></i>{{ item.Name }} </span>
<span ng-if="!item.edit && !item.Dir" class="vertical-center"><pr-icon icon="'file'" feather="true"></pr-icon>{{ item.Name }}</span>
</td>
<td>{{ item.Size | humansize }}</td>
<td>
{{ item.ModTime | getisodatefromtimestamp }}
</td>
<td>
<btn authorization="DockerAgentBrowseGet" class="btn btn-xs btn-primary space-right" ng-click="$ctrl.download({ name: item.Name })" ng-if="!item.Dir">
<i class="fa fa-download" aria-hidden="true"></i> Download
<btn authorization="DockerAgentBrowseGet" class="btn btn-xs btn-secondary space-right" ng-click="$ctrl.download({ name: item.Name })" ng-if="!item.Dir">
<pr-icon icon="'download'" feather="true"></pr-icon> Download
</btn>
<btn authorization="DockerAgentBrowseRename" class="btn btn-xs btn-primary space-right" ng-click="item.newName = item.Name; item.edit = true">
<i class="fa fa-edit" aria-hidden="true"></i> Rename
<btn authorization="DockerAgentBrowseRename" class="btn btn-xs btn-secondary space-right" ng-click="item.newName = item.Name; item.edit = true">
<pr-icon icon="'edit'" feather="true"></pr-icon> Rename
</btn>
<btn authorization="DockerAgentBrowseDelete" class="btn btn-xs btn-danger" ng-click="$ctrl.delete({ name: item.Name })">
<i class="fa fa-trash" aria-hidden="true"></i> Delete
<btn authorization="DockerAgentBrowseDelete" class="btn btn-xs btn-dangerlight" ng-click="$ctrl.delete({ name: item.Name })">
<pr-icon icon="'trash-2'" feather="true"></pr-icon> Delete
</btn>
</td>
</tr>
@@ -1,6 +1,6 @@
<files-datatable
title-text="Host browser - {{ $ctrl.getRelativePath() }}"
title-icon="fa-file"
title-icon="file"
dataset="$ctrl.files"
table-key="host_browser"
order-by="Dir"
@@ -1,6 +1,6 @@
<files-datatable
title-text="Volume browser"
title-icon="fa-file"
title-icon="file"
dataset="$ctrl.files"
table-key="volume_browser"
order-by="Dir"
+4 -2
View File
@@ -1,6 +1,6 @@
import _ from 'lodash';
import { useSettings } from '@/portainer/settings/queries';
import { usePublicSettings } from '@/portainer/settings/queries';
const categories = [
'docker',
@@ -64,7 +64,9 @@ export function push(
}
export function useAnalytics() {
const telemetryQuery = useSettings((settings) => settings.EnableTelemetry);
const telemetryQuery = usePublicSettings({
select: (settings) => settings.EnableTelemetry,
});
return { trackEvent: handleTrackEvent };
+8 -1
View File
@@ -6,7 +6,14 @@ export function onStartupAngular($rootScope, $state, $interval, LocalStorage, En
EndpointProvider.initialize();
$rootScope.$state = $state;
$rootScope.defaultTitle = document.title;
const defaultTitle = document.title;
$transitions.onEnter({}, () => {
const endpoint = EndpointProvider.currentEndpoint();
if (endpoint) {
document.title = `${defaultTitle} | ${endpoint.Name}`;
}
});
// Workaround to prevent the loading bar from going backward
// https://github.com/chieffancypants/angular-loading-bar/issues/273
+89 -43
View File
@@ -2,8 +2,27 @@
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Inter';
src: url('../fonts/Inter-VariableFont.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
select {
font-family: Inter, Arial, Helvetica, sans-serif;
}
}
html {
font-size: 16px;
overflow-y: scroll;
}
body {
background: var(--bg-body-color);
font-family: 'Inter';
color: var(--text-body-color) !important;
}
html,
@@ -21,15 +40,16 @@ body,
position: relative;
}
.white-space-normal {
white-space: normal !important;
}
.logo {
display: inline;
max-width: 155px;
max-height: 55px;
}
.white-space-normal {
white-space: normal !important;
}
.legend .title {
padding: 0 0.3em;
margin: 0.5em;
@@ -62,16 +82,16 @@ body,
font-size: 18px;
}
.header_title_content {
margin-left: 5px;
}
.form-section-title {
border-bottom: 1px solid var(--border-form-section-title-color);
@apply text-gray-9;
@apply th-dark:text-gray-5;
@apply th-highcontrast:text-white;
margin-top: 5px;
margin-bottom: 15px;
color: var(--text-form-section-title-color);
margin-bottom: 10px;
padding-left: 0;
font-weight: 500;
font-size: 16px;
}
.form-horizontal .control-label.text-left {
@@ -97,10 +117,6 @@ input[type='checkbox'] {
text-align: center;
}
a[ng-click] {
cursor: pointer;
}
.space-right {
margin-right: 5px;
}
@@ -131,25 +147,12 @@ a[ng-click] {
background-color: var(--bg-item-highlighted-null-color);
}
.service-datatable {
background-color: var(--bg-item-highlighted-color);
padding: 2px;
}
.service-datatable thead {
background-color: var(--bg-service-datatable-thead) !important;
}
.service-datatable tbody {
background-color: var(--bg-service-datatable-tbody);
}
.fa.green-icon {
color: #23ae89;
}
.fa.red-icon {
color: #ae2323;
color: #f04438;
}
.fa.orange-icon {
@@ -221,12 +224,12 @@ a[ng-click] {
}
.blocklist-item {
padding: 7px;
margin-bottom: 7px;
padding: 10px;
margin-bottom: 10px;
cursor: pointer;
border: 1px solid var(--border-blocklist-color);
border-radius: 2px;
box-shadow: var(--shadow-box-color);
border: 1px solid var(--border-blocklist);
border-radius: 8px;
margin-right: 10px;
}
.blocklist-item--disabled {
@@ -241,6 +244,8 @@ a[ng-click] {
}
.blocklist-item:hover {
@apply border border-blue-7;
background-color: var(--bg-blocklist-hover-color);
color: var(--text-blocklist-hover-color);
}
@@ -261,7 +266,6 @@ a[ng-click] {
.blocklist-item-logo {
width: 100%;
max-width: 60px;
height: 100%;
max-height: 60px;
}
@@ -379,12 +383,13 @@ a[ng-click] {
}
.panel-body {
padding-top: 30px;
background-color: var(--white-color) fff;
padding: 20px 25px;
background-color: var(--white-color);
border-radius: 8px;
}
.user-box {
margin-right: 25px;
margin-right: 15px;
}
.select-endpoint {
@@ -472,7 +477,7 @@ a[ng-click] {
:root[theme='dark'] .bootbox-checkbox-list,
:root[theme='highcontrast'] .bootbox-checkbox-list {
background-color: var(--black-color);
background-color: var(--bg-modal-content-color);
}
.small-select {
@@ -807,11 +812,12 @@ json-tree .branch-preview {
}
/* !spinkit override */
.kubectl-shell {
display: block;
text-align: center;
padding-bottom: 5px;
/* uib-typeahead override */
#scrollable-dropdown-menu .dropdown-menu {
max-height: 300px;
overflow-y: auto;
}
/* !uib-typeahead override */
.no-margin {
margin: 0 !important;
@@ -835,3 +841,43 @@ json-tree .branch-preview {
.form-check.radio {
margin-left: 15px;
}
.inline-text {
display: inline;
position: absolute;
font-family: 'Montserrat';
font-size: smaller;
margin-left: 5px;
margin-right: 5px;
}
.web-editor {
background-color: var(--bg-webeditor-color);
border-radius: 8px;
padding: 10px;
}
.web-editor a {
color: var(--text-link-color);
}
.web-editor a:hover {
color: var(--text-link-hover-color);
text-decoration-line: underline;
}
reach-portal > div {
z-index: 10;
}
input[style*='background-image: url("data:image/png'] + [data-cy='auth-passwordInputToggle'] {
right: 20px;
}
input[style*='background-image: url("data:image/png'] {
padding-right: 60px;
}
.web-editor .trancluded-item:empty {
display: none;
}
+409
View File
@@ -0,0 +1,409 @@
/* Label, Section Title */
.label {
border-radius: 5px;
}
.label-success {
background-color: var(--ui-success-7);
}
.label-danger {
background-color: var(--ui-error-6);
}
.control-label {
@apply inline-flex items-center;
@apply font-medium;
@apply text-gray-7;
@apply th-dark:text-gray-warm-3;
@apply th-highcontrast:text-white;
}
.vertical-center {
display: inline-flex;
align-items: center;
gap: 5px;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.blue {
background: var(--bg-dashboard-item) !important;
}
.form-control {
border-radius: 5px;
}
/* Input Group Addon */
.input-group-addon:first-child {
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
}
.input-group .form-control:not(:first-child):not(:last-child) {
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.input-group-btn:last-child .btn {
margin-left: 5px;
border-radius: 5px;
}
/* Toggle switch */
.switch {
position: relative;
display: inline-block;
width: 42px;
height: 25px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.switch input[type='checkbox']:disabled + .slider {
background-color: var(--ui-gray-3);
@apply th-dark:before:bg-gray-warm-8;
@apply th-highcontrast:before:bg-gray-warm-8;
@apply th-dark:bg-gray-warm-9;
@apply th-highcontrast:bg-gray-warm-9;
}
.switch-values {
font-style: normal;
font-weight: 500;
margin-left: 5px;
}
/* Toggle */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--bg-switch-box-color);
-webkit-transition: 0.4s;
transition: 0.4s;
@apply th-dark:bg-gray-warm-9;
@apply th-highcontrast:bg-gray-warm-9;
}
.slider:before {
position: absolute;
content: '';
height: 19px;
width: 19px;
left: 3px;
bottom: 3px;
background-color: var(--white-color);
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .slider {
background-color: var(--ui-blue-8);
@apply th-dark:bg-blue-9;
@apply th-highcontrast:bg-blue-9;
}
input:focus + .slider {
box-shadow: 0 0 1px var(--ui-blue-8);
}
input:checked + .slider:before {
-webkit-transform: translateX(17px);
-ms-transform: translateX(17px);
transform: translateX(17px);
}
.slider.round {
border-radius: 25px;
}
.slider.round:before {
border-radius: 50%;
}
/* Checkbox */
.md-checkbox input[type='checkbox']:enabled + label:before {
background-color: var(--bg-checkbox) !important;
border: 1px solid var(--border-checkbox) !important;
border-radius: 5px;
}
.md-checkbox input[type='checkbox']:disabled + label:before {
border-radius: 5px;
}
.md-checkbox input[type='checkbox']:checked + label:before {
background-color: var(--ui-blue-8) !important;
color: var(--ui-blue-8) !important;
border: 1px solid var(--ui-blue-8) !important;
}
.md-checkbox input[type='checkbox']:checked + .checkmark {
border-color: var(--grey-6);
background-color: var(--bg-checkbox);
}
/* Slider */
.rzslider .rz-pointer {
background-color: var(--white-color);
border: 3px solid var(--ui-blue-8);
width: 25px;
height: 25px;
top: -10px;
}
.rzslider .rz-bar {
background-color: var(--ui-gray-5);
height: 8px;
border-radius: 5px;
}
.rzslider .rz-selection {
background-color: var(--ui-blue-8);
}
.rzslider .rz-pointer:after {
display: none;
}
/* Widget */
.widget .widget-icon {
@apply text-lg !p-2 mr-1;
@apply bg-blue-3 text-blue-8;
@apply th-dark:bg-gray-9 th-dark:text-blue-3;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
padding: 1.5%;
}
.widget .widget-body table thead {
border-top: 1px solid var(--border-table-color);
}
/* Toaster */
#toast-container > .toast-success {
background-image: url(../images/icon-success.svg) !important;
background-size: 40px 40px;
background-position: top 12px left 12px;
}
#toast-container > .toast-error {
background-image: url(../images/icon-error.svg) !important;
background-size: 40px 40px;
background-position: top 12px left 12px;
}
#toast-container > .toast-warning {
background-image: url(../images/icon-warning.svg) !important;
background-size: 40px 40px;
background-position: top 12px left 12px;
}
.toast-success .toast-progress {
background-color: var(--ui-success-7);
}
.toast-warning .toast-progress {
background-color: var(--ui-warning-6);
}
.toast-error .toast-progress {
background-color: var(--ui-error-8);
}
#toast-container > div {
color: var(--ui-gray-7);
background-color: var(--white-color);
border-radius: 8px;
padding: 18px 20px 18px 68px;
width: 300px;
opacity: 1;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
filter: alpha(opacity=100);
}
#toast-container > div:hover {
-moz-box-shadow: 0 0 12px var(--ui-gray-7);
-webkit-box-shadow: 0 0 12px var(--ui-gray-7);
box-shadow: 0 0 12px var(--ui-gray-7);
}
.toast-close-button {
color: var(--black-color);
text-decoration: none;
margin-top: 5px;
cursor: pointer;
opacity: 0.4;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
filter: alpha(opacity=40);
}
.toast-close-button:hover,
.toast-close-button:focus {
color: var(--black-color);
text-decoration: none;
cursor: pointer;
opacity: 0.6;
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=60);
filter: alpha(opacity=60);
}
.toast-title {
font-weight: 500;
color: var(--black-color);
padding-right: 10px;
margin-bottom: 4px;
}
/* Modal */
.modal-dialog {
width: 450px;
}
.modal-content {
padding: 20px;
}
.background-error {
padding-top: 55px;
background-image: url(../images/icon-error.svg);
background-repeat: no-repeat;
background-position: top left;
}
.background-warning {
padding-top: 55px;
background-image: url(../images/icon-warning.svg);
background-repeat: no-repeat;
background-position: top left;
}
.modal-header {
margin-bottom: 10px;
padding: 0px;
border-bottom: none;
}
.modal-header .close {
margin-top: 0px;
}
.modal-header .modal-title {
font-weight: bold;
}
.modal-body {
padding: 10px 0px;
border-bottom: none;
}
.modal-body .bootbox-body {
font-size: 12px;
color: var(--text-bootbox);
}
.modal-footer {
padding: 10px 0px;
border-top: none;
display: flex;
}
.modal-footer .bootbox-cancel {
width: 100%;
}
.modal-footer .bootbox-accept {
width: 100%;
}
.bootbox-checkbox-list {
border: 0px;
}
/* Status Indicator Inside Table Section Label Style */
.table .label {
border-radius: 8px !important;
display: inline-flex;
align-items: center;
}
.table .label .label-danger {
background-color: var(--ui-error-8);
}
.table .label .label-warn {
background-color: var(--ui-warning-8);
}
.table .label .label-success {
background-color: var(--ui-success-7);
}
/* Required Label with asterisk */
.required:after {
content: '*';
color: var(--ui-error-9);
}
.progress {
height: 8px;
border-radius: 4px;
width: 50%;
display: inline-block;
margin-bottom: 0;
}
.progress .progress-bar {
background-color: var(--ui-blue-8);
}
.progress + span {
display: inline-block;
font-size: 85%;
margin-left: 10px;
}
/* Pagination */
.datatable .footer .paginationControls .pagination {
border: 1px solid var(--border-pagination-color);
}
.pagination li button {
color: var(--ui-gray-9) !important;
}
.pagination li:active button,
.pagination li:focus button {
border: 1px solid var(--ui-gray-5) !important;
}
.pagination li a {
text-decoration: none !important;
cursor: pointer;
color: var(--ui-gray-9) !important;
}
.widget-header {
font-size: 16px;
}
+161
View File
@@ -0,0 +1,161 @@
.btn {
@apply !outline-none;
@apply border border-solid border-transparent;
border-radius: 8px;
display: inline-flex;
justify-content: space-around;
align-items: center;
gap: 5px;
}
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn {
@apply opacity-40;
pointer-events: none;
touch-action: none;
}
.btn:hover {
color: var(--text-button-hover-color);
}
.btn.active {
box-shadow: none;
}
.btn.btn-primary {
@apply text-white bg-blue-8 border-blue-8;
@apply hover:text-white hover:bg-blue-9 hover:border-blue-9;
@apply th-dark:hover:bg-blue-7 th-dark:hover:border-blue-7;
}
.btn.btn-primary:active,
.btn.btn-primary.active,
.open > .dropdown-toggle.btn-primary {
@apply bg-blue-9 border-blue-5;
}
.nav-pills > li.active > a,
.nav-pills > li.active > a:hover,
.nav-pills > li.active > a:focus {
@apply bg-blue-8;
}
/* Button Secondary */
.btn.btn-secondary {
@apply border border-solid;
@apply text-blue-9 bg-blue-2 border-blue-8;
@apply hover:bg-blue-3;
@apply th-dark:text-blue-3 th-dark:bg-gray-10 th-dark:border-blue-7;
@apply th-dark:hover:bg-blue-11;
}
.btn.btn-danger {
@apply bg-error-8 border-error-8;
@apply hover:bg-error-7 hover:border-error-7 hover:text-white;
}
.btn.btn-danger:active,
.btn.btn-danger.active,
.open > .dropdown-toggle.btn-danger {
@apply bg-error-8 text-white border-blue-5;
}
.btn.btn-dangerlight {
@apply text-error-9 th-dark:text-white;
@apply bg-error-3 th-dark:bg-error-9;
@apply hover:bg-error-2 th-dark:hover:bg-error-11;
@apply border-error-5 th-dark:border-error-7 th-highcontrast:border-error-7;
@apply border border-solid;
}
.btn.btn-success {
background-color: var(--ui-success-7);
}
.btn.btn-success:hover {
color: var(--white-color);
}
/* secondary-grey */
.btn.btn-default,
.btn.btn-light {
@apply bg-white border-gray-5 text-gray-9;
@apply hover:bg-gray-3 hover:border-gray-5 hover:text-gray-10;
/* dark mode */
@apply th-dark:bg-gray-iron-10 th-dark:border-gray-warm-7 th-dark:text-gray-warm-4;
@apply th-dark:hover:bg-gray-iron-9 th-dark:hover:border-gray-6 th-dark:hover:text-gray-warm-4;
@apply th-highcontrast:bg-black th-highcontrast:border-gray-2 th-highcontrast:text-white;
@apply th-highcontrast:hover:bg-gray-9 th-highcontrast:hover:border-gray-6 th-highcontrast:hover:text-gray-warm-4;
}
.btn.btn-light:active,
.btn.btn-light.active,
.open > .dropdown-toggle.btn-light {
background-color: var(--ui-gray-3);
}
.btn.btn-link {
@apply text-blue-8 hover:text-blue-9 disabled:text-gray-5;
@apply th-dark:text-blue-8 th-dark:hover:text-blue-7;
@apply th-highcontrast:text-blue-8 th-highcontrast:hover:text-blue-7;
}
.btn-group {
display: inline-flex;
}
.input-group-btn .btn.active,
.btn-group .btn.active {
@apply bg-blue-2 text-blue-10 border-blue-5;
@apply th-dark:bg-blue-11 th-dark:text-blue-2 th-dark:border-blue-9;
}
/* focus */
.btn.btn-primary:focus,
.btn.btn-secondary:focus,
.btn.btn-light:focus {
@apply border-blue-5;
}
.btn.btn-danger:focus,
.btn.btn-dangerlight:focus {
@apply border-blue-6;
}
.btn.btn-primary:focus,
.btn.btn-secondary:focus,
.btn.btn-light:focus,
.btn.btn-danger:focus,
.btn.btn-dangerlight:focus {
--btn-focus-color: var(--ui-blue-3);
box-shadow: 0px 0px 0px 4px var(--btn-focus-color);
}
[theme='dark'] .btn.btn-primary:focus,
[theme='dark'] .btn.btn-secondary:focus,
[theme='dark'] .btn.btn-light:focus,
[theme='dark'] .btn.btn-danger:focus,
[theme='dark'] .btn.btn-dangerlight:focus {
--btn-focus-color: var(--ui-blue-11);
}
a.no-link,
a[ng-click] {
@apply text-current;
@apply hover:no-underline hover:text-current;
@apply focus:no-underline focus:text-current;
}
a,
a.hyperlink {
@apply text-blue-8 hover:text-blue-9;
@apply hover:underline cursor-pointer;
}
+355
View File
@@ -0,0 +1,355 @@
{
"black": "#000000",
"white": "#ffffff",
"gray": {
"1": "#fcfcfd",
"2": "#f9fafb",
"3": "#f2f4f7",
"4": "#eaecf0",
"5": "#d0d5dd",
"6": "#98a2b3",
"7": "#667085",
"8": "#475467",
"9": "#344054",
"10": "#1d2939",
"11": "#101828"
},
"blue": {
"1": "#f5fbff",
"2": "#f0f9ff",
"3": "#e0f2fe",
"4": "#b9e6fe",
"5": "#7cd4fd",
"6": "#36bffa",
"7": "#0ba5ec",
"8": "#0086c9",
"9": "#026aa2",
"10": "#065986",
"11": "#0b4a6f"
},
"error": {
"1": "#fffbfa",
"2": "#fef3f2",
"3": "#fee4e2",
"4": "#fecdca",
"5": "#fda29b",
"6": "#f97066",
"7": "#f04438",
"8": "#d92d20",
"9": "#b42318",
"10": "#912018",
"11": "#7a271a"
},
"warning": {
"1": "#fffcf5",
"2": "#fffaeb",
"3": "#fef0c7",
"4": "#fedf89",
"5": "#fec84b",
"6": "#fdb022",
"7": "#f79009",
"8": "#dc6803",
"9": "#b54708",
"10": "#93370d",
"11": "#7a2e0e"
},
"success": {
"1": "#f6fef9",
"2": "#ecfdf3",
"3": "#d1fadf",
"4": "#a6f4c5",
"5": "#6ce9a6",
"6": "#32d583",
"7": "#12b76a",
"8": "#039855",
"9": "#027a48",
"10": "#05603a",
"11": "#054f31"
},
"gray-blue": {
"1": "#fcfcfd",
"2": "#f8f9fc",
"3": "#eaecf5",
"4": "#d5d9eb",
"5": "#b3b8db",
"6": "#717bbc",
"7": "#4e5ba6",
"8": "#3e4784",
"9": "#363f72",
"10": "#293056",
"11": "#293056"
},
"gray-cool": {
"1": "#fcfcfd",
"2": "#f9f9fb",
"3": "#eff1f5",
"4": "#dcdfea",
"5": "#b9c0d4",
"6": "#7d89b0",
"7": "#5d6b98",
"8": "#4a5578",
"9": "#404968",
"10": "#30374f",
"11": "#111322"
},
"gray-modern": {
"1": "#fcfcfd",
"2": "#f8fafc",
"3": "#eef2f6",
"4": "#e3e8ef",
"5": "#cdd5df",
"6": "#9aa4b2",
"7": "#697586",
"8": "#4b5565",
"9": "#364152",
"10": "#202939",
"11": "#121926"
},
"gray-neutral": {
"1": "#fcfcfd",
"2": "#f9fafb",
"3": "#f3f4f6",
"4": "#e5e7eb",
"5": "#d2d6db",
"6": "#9da4ae",
"7": "#6c737f",
"8": "#4d5761",
"9": "#384250",
"10": "#1f2a37",
"11": "#111927"
},
"gray-iron": {
"1": "#fcfcfc",
"2": "#fafafa",
"3": "#f4f4f5",
"4": "#e4e4e7",
"5": "#d1d1d6",
"6": "#d1d1d6",
"7": "#70707b",
"8": "#51525c",
"9": "#3f3f46",
"10": "#26272b",
"11": "#18181b"
},
"gray-true": {
"1": "#fcfcfc",
"2": "#fafafa",
"3": "#f5f5f5",
"4": "#e5e5e5",
"5": "#d6d6d6",
"6": "#a3a3a3",
"7": "#737373",
"8": "#525252",
"9": "#424242",
"10": "#292929",
"11": "#141414"
},
"gray-warm": {
"1": "#fdfdfc",
"2": "#fafaf9",
"3": "#f5f5f4",
"4": "#e7e5e4",
"5": "#d7d3d0",
"6": "#a9a29d",
"7": "#79716b",
"8": "#57534e",
"9": "#44403c",
"10": "#292524",
"11": "#1c1917"
},
"moss": {
"1": "#fafdf7",
"2": "#f5fbee",
"3": "#e6f4d7",
"4": "#ceeab0",
"5": "#acdc79",
"6": "#86cb3c",
"7": "#669f2a",
"8": "#4f7a21",
"9": "#3f621a",
"10": "#335015",
"11": "#2b4212"
},
"green-light": {
"1": "#fafef5",
"2": "#f3fee7",
"3": "#e4fbcc",
"4": "#d0f8ab",
"5": "#a6ef67",
"6": "#85e13a",
"7": "#66c61c",
"8": "#4ca30d",
"9": "#3b7c0f",
"10": "#326212",
"11": "#2b5314"
},
"green": {
"1": "#f6fef9",
"2": "#edfcf2",
"3": "#d3f8df",
"4": "#aaf0c4",
"5": "#73e2a3",
"6": "#73e2a3",
"7": "#16b364",
"8": "#099250",
"9": "#087443",
"10": "#095c37",
"11": "#084c2e"
},
"teal": {
"1": "#f6fefc",
"2": "#f0fdf9",
"3": "#ccfbef",
"4": "#99f6e0",
"5": "#5fe9d0",
"6": "#2ed3b7",
"7": "#15b79e",
"8": "#0e9384",
"9": "#107569",
"10": "#125d56",
"11": "#134e48"
},
"cyan": {
"1": "#f5feff",
"2": "#ecfdff",
"3": "#cff9fe",
"4": "#a5f0fc",
"5": "#67e3f9",
"6": "#22ccee",
"7": "#06aed4",
"8": "#088ab2",
"9": "#0e7090",
"10": "#155b75",
"11": "#164c63"
},
"blue-dark": {
"1": "#f5f8ff",
"2": "#eff4ff",
"3": "#d1e0ff",
"4": "#b2ccff",
"5": "#84adff",
"6": "#528bff",
"7": "#2970ff",
"8": "#155eef",
"9": "#004eeb",
"10": "#0040c1",
"11": "#00359e"
},
"indigo": {
"1": "#f5f8ff",
"2": "#eef4ff",
"3": "#e0eaff",
"4": "#c7d7fe",
"5": "#a4bcfd",
"6": "#8098f9",
"7": "#8098f9",
"8": "#444ce7",
"9": "#3538cd",
"10": "#2d31a6",
"11": "#2d3282"
},
"violet": {
"1": "#fbfaff",
"2": "#f5f3ff",
"3": "#ece9fe",
"4": "#ddd6fe",
"5": "#c3b5fd",
"6": "#a48afb",
"7": "#875bf7",
"8": "#7839ee",
"9": "#6927da",
"10": "#5720b7",
"11": "#491c96"
},
"purple": {
"1": "#fafaff",
"2": "#f4f3ff",
"3": "#ebe9fe",
"4": "#d9d6fe",
"5": "#bdb4fe",
"6": "#9b8afb",
"7": "#7a5af8",
"8": "#6938ef",
"9": "#5925dc",
"10": "#4a1fb8",
"11": "#3e1c96"
},
"fuchsia": {
"1": "#fefaff",
"2": "#fdf4ff",
"3": "#fbe8ff",
"4": "#f6d0fe",
"5": "#eeaafd",
"6": "#e478fa",
"7": "#d444f1",
"8": "#ba24d5",
"9": "#9f1ab1",
"10": "#821890",
"11": "#6f1877"
},
"pink": {
"1": "#fef6fb",
"2": "#fdf2fa",
"3": "#fce7f6",
"4": "#fce7f6",
"5": "#faa7e0",
"6": "#f670c7",
"7": "#ee46bc",
"8": "#dd2590",
"9": "#c11574",
"10": "#9e165f",
"11": "#851651"
},
"rose": {
"1": "#fff5f6",
"2": "#fff1f3",
"3": "#ffe4e8",
"4": "#fecdd6",
"5": "#fea3b4",
"6": "#fd6f8e",
"7": "#f63d68",
"8": "#e31b54",
"9": "#c01048",
"10": "#a11043",
"11": "#89123e"
},
"orange-dark": {
"1": "#fff9f5",
"2": "#fff4ed",
"3": "#ffe6d5",
"4": "#ffd6ae",
"5": "#ff9c66",
"6": "#ff692e",
"7": "#ff4405",
"8": "#e62e05",
"9": "#bc1b06",
"10": "#97180c",
"11": "#771a0d"
},
"orange": {
"1": "#fefaf5",
"2": "#fef6ee",
"3": "#fdead7",
"4": "#f9dbaf",
"5": "#f7b27a",
"6": "#f38744",
"7": "#ef6820",
"8": "#e04f16",
"9": "#b93815",
"10": "#932f19",
"11": "#772917"
},
"yellow": {
"1": "#fefdf0",
"2": "#fefbe8",
"3": "#fef7c3",
"4": "#feee95",
"5": "#feee95",
"6": "#fac515",
"7": "#eaaa08",
"8": "#ca8504",
"9": "#a15c07",
"10": "#854a0e",
"11": "#713b12"
}
}
+19
View File
@@ -0,0 +1,19 @@
import colors from './colors.json';
const element = document.createElement('style');
element.innerHTML = `:root {
${Object.entries(colors)
.map(([color, hex]) => {
if (typeof hex === 'string') {
return `--ui-${color}: ${hex}`;
}
return Object.entries(hex)
.map(([key, value]) => `--ui-${color}-${key}: ${value}`)
.join(';\n');
})
.join(';\n')}
}`;
document.head.prepend(element);
+133
View File
@@ -0,0 +1,133 @@
.feather {
display: block;
height: 1em;
width: 1em;
color: inherit;
}
pr-icon {
display: inline-flex;
}
.icon {
color: currentColor;
margin: 0;
display: inline-block;
font-size: var(--icon-size);
height: var(--icon-size);
width: var(--icon-size);
--icon-size: 1em;
}
.icon-xs {
--icon-size: 10px;
}
.icon-sm {
--icon-size: 14px;
}
.icon-md {
--icon-size: 16px;
}
.icon-lg {
--icon-size: 22px;
}
.icon-xl {
--icon-size: 26px;
}
.icon.icon-alt {
fill: var(--black-color);
stroke: var(--white-color);
}
.icon-primary,
.icon-blue {
color: var(--ui-blue-8);
}
.icon.icon-primary-alt {
fill: var(--ui-blue-8);
stroke: var(--white-color);
}
.icon-secondary {
color: var(--ui-gray-8);
}
.icon.icon-secondary-alt {
fill: var(--ui-gray-8);
stroke: var(--black-color);
}
.icon-warning,
.icon-orange {
color: var(--ui-warning-8);
}
.icon.icon-warning-alt {
fill: var(--ui-warning-8);
stroke: var(--white-color);
}
.icon-danger {
color: var(--ui-error-9);
}
.icon.icon-danger-alt {
fill: var(--ui-error-9);
stroke: var(--white-color);
}
.icon-success {
color: var(--ui-success-6);
}
.icon.icon-success-alt {
fill: var(--ui-success-8);
stroke: var(--white-color);
}
.icon-badge {
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
padding: 1.5%;
}
.icon-nested-blue {
display: flex;
justify-content: center;
align-items: center;
height: 30px;
width: 30px;
padding: 5px;
text-align: center;
border-radius: 50%;
background-color: var(--ui-blue-3);
margin-right: 5px;
}
.icon-nested-blue > svg {
height: 20px;
width: 20px;
}
.icon-container {
display: flex;
align-items: center;
}
.btn-only-icon {
padding: 6px;
}
.btn-only-icon pr-icon {
margin-top: 0;
}
+6
View File
@@ -15,8 +15,14 @@ import 'angular-multiselect/isteven-multi-select.css';
import 'spinkit/spinkit.min.css';
import '@reach/menu-button/styles.css';
import './colors';
import './rdash.css';
import './app.css';
import './theme.css';
import './vendor-override.css';
import '../fonts/nomad-icon.css';
import './bootstrap-override.css';
import './icon.css';
import './button.css';
+10 -111
View File
@@ -4,49 +4,6 @@
width: 100%;
height: auto;
}
@media only screen and (min-width: 561px) {
#page-wrapper.open {
padding-left: 250px;
}
}
@media only screen and (max-width: 560px) {
#page-wrapper.open {
padding-left: 70px;
}
}
/**
* Hamburg Menu
* When the class of 'hamburg' is applied to the body tag of the document,
* the sidebar changes it's style to attempt to mimic a menu on a phone app,
* where the content is overlaying the content, rather than push it.
*/
@media only screen and (max-width: 560px) {
body.hamburg #page-wrapper {
padding-left: 0;
}
body.hamburg #page-wrapper:not(.open) #sidebar-wrapper {
position: absolute;
left: -100px;
}
body.hamburg #page-wrapper:not(.open) ul.sidebar .sidebar-title.separator {
display: none;
}
body.hamburg #page-wrapper.open #sidebar-wrapper {
position: fixed;
}
body.hamburg #page-wrapper.open #sidebar-wrapper ul.sidebar li.sidebar-main {
margin-left: 0px;
}
body.hamburg #sidebar-wrapper ul.sidebar li.sidebar-main,
body.hamburg .row.header .meta {
margin-left: 70px;
}
body.hamburg #sidebar-wrapper ul.sidebar li.sidebar-main,
body.hamburg #page-wrapper.open #sidebar-wrapper ul.sidebar li.sidebar-main {
transition: margin-left 0.4s ease 0s;
}
}
.loading {
width: 40px;
@@ -92,33 +49,6 @@
}
}
/* Fonts */
@font-face {
font-family: 'Montserrat';
src: url('../fonts/montserrat-regular-webfont.eot');
src: url('../fonts/montserrat-regular-webfont.eot?#iefix') format('embedded-opentype'), url('../fonts/montserrat-regular-webfont.woff') format('woff'),
url('../fonts/montserrat-regular-webfont.ttf') format('truetype'), url('../fonts/montserrat-regular-webfont.svg#montserratregular') format('svg');
font-weight: normal;
font-style: normal;
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
@font-face {
font-family: 'Montserrat';
src: url('../fonts/montserrat-regular-webfont.svg') format('svg');
}
select {
font-family: Arial, Helvetica, sans-serif;
}
}
/* Base */
html {
overflow-y: scroll;
}
body {
background: var(--bg-body-color);
font-family: 'Montserrat';
color: var(--text-body-color) !important;
}
.row {
margin-left: 0 !important;
margin-right: 0 !important;
@@ -129,22 +59,7 @@ body {
.alerts-container .alert:last-child {
margin-bottom: 0;
}
#page-wrapper {
padding-left: 70px;
height: 100%;
}
#sidebar-wrapper {
margin-left: -150px;
left: -30px;
width: 250px;
position: fixed;
height: 100%;
z-index: 999;
}
#page-wrapper,
#sidebar-wrapper {
transition: all 0.4s ease 0s;
}
.green {
background: #23ae89 !important;
}
@@ -172,9 +87,8 @@ div.input-mask {
-moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
background: var(--bg-widget-color);
border: 1px solid transparent;
border-radius: 2px;
border-color: var(--border-widget-color);
border: 1px solid var(--border-widget);
border-radius: 8px;
}
.widget .widget-header .pagination,
.widget .widget-footer .pagination {
@@ -182,25 +96,23 @@ div.input-mask {
}
.widget .widget-header {
color: var(--text-widget-header-color);
background-color: var(--bg-widget-header-color);
padding: 10px 15px;
border-bottom: 1px solid var(--border-widget-color);
padding: 20px 20px 10px 20px;
line-height: 30px;
font-weight: 500;
}
.widget .widget-header i {
margin-right: 5px;
}
.widget .widget-body {
padding: 20px;
border-radius: 8px;
}
.widget .widget-body table thead {
background: var(--bg-widget-table-color);
}
.widget .widget-body table thead * {
font-size: 14px !important;
font-size: 14px;
}
.widget .widget-body table tbody * {
font-size: 13px !important;
font-size: 13px;
}
.widget .widget-body .error {
color: #ff0000;
@@ -234,20 +146,7 @@ div.input-mask {
border-top: 1px solid #e9e9e9;
padding: 10px;
}
.widget .widget-icon {
background: #30426a;
width: 65px;
height: 65px;
border-radius: 50%;
text-align: center;
vertical-align: middle;
margin-right: 15px;
}
.widget .widget-icon i {
line-height: 66px;
color: #ffffff;
font-size: 30px;
}
.widget .widget-footer {
border-top: 1px solid #e9e9e9;
padding: 10px;
+192 -152
View File
@@ -1,13 +1,12 @@
/* Color Variable */
html {
--black-color: #000;
--white-color: #fff;
:root {
--black-color: var(--ui-black);
--white-color: var(--ui-white);
--grey-1: #212121;
--grey-2: #181818;
--grey-3: #383838;
--grey-4: #585858;
--grey-5: #323c48;
--grey-6: #333333;
--grey-7: #767676;
--grey-8: #aaa;
@@ -35,7 +34,6 @@ html {
--grey-30: #444;
--grey-31: #868686;
--grey-32: #65798e;
--grey-34: #314252;
--grey-35: #546477;
--grey-36: #55637d;
--grey-37: #2d3e63;
@@ -53,15 +51,12 @@ html {
--grey-49: rgba(0, 0, 0, 0.54);
--grey-50: rgba(161, 170, 166, 0.5);
--grey-51: rgba(0, 0, 0, 0.15);
--grey-52: rgba(255, 255, 255, 0.3);
--grey-53: rgba(255, 255, 255, 0.6);
--grey-54: rgb(54, 54, 54);
--grey-55: rgba(255, 255, 255, 0.8);
--grey-56: #b2bfdc;
--grey-57: #999;
--grey-58: #ebf4f8;
--grey-59: #e6e6e6;
--grey-60: #cacaca;
--grey-61: rgb(231, 231, 231);
--blue-1: #219;
@@ -76,7 +71,6 @@ html {
--blue-10: #61b6ff;
--blue-11: #3ea5ff;
--blue-12: #41a6ff;
--blue-13: #2361ae;
--blue-14: #357ebd;
--red-1: #a94442;
@@ -84,7 +78,6 @@ html {
--red-3: #a11;
--red-4: #d9534f;
--red-5: #ff2727;
--red-6: #ff00e0;
--red-7: #f00;
--green-1: #164;
@@ -93,35 +86,34 @@ html {
--orange-1: #e86925;
--BE-only: var(--orange-1);
}
--BE-only: var(--ui-warning-7);
/* Default Theme */
:root {
--bg-card-color: var(--grey-10);
/* Default Theme */
--bg-card-color: var(--white-color);
--bg-main-color: var(--white-color);
--bg-body-color: var(--grey-9);
--bg-checkbox-border-color: var(--grey-49);
--bg-sidebar-color: var(--grey-37);
--bg-sidebar-header-color: var(--grey-37);
--bg-sidebar-color: var(--ui-blue-10);
--bg-sidebar-nav-color: var(--ui-blue-11);
--bg-widget-color: var(--white-color);
--bg-widget-header-color: var(--grey-10);
--bg-widget-table-color: var(--grey-13);
--bg-widget-table-color: var(--ui-gray-3);
--bg-header-color: var(--white-color);
--bg-hover-table-color: var(--grey-14);
--bg-switch-box-color: var(--white-color);
--bg-input-group-addon-color: var(--grey-11);
--bg-btn-default-color: var(--white-color);
--bg-blocklist-hover-color: var(--grey-12);
--bg-boxselector-color: var(--white-color);
--bg-switch-box-color: var(--ui-gray-5);
--bg-input-group-addon-color: var(--ui-gray-3);
--bg-btn-default-color: var(--ui-blue-10);
--bg-blocklist-hover-color: var(--ui-blue-2);
--bg-boxselector-color: var(--ui-gray-2);
--bg-table-color: var(--white-color);
--bg-md-checkbox-color: var(--grey-12);
--bg-form-control-disabled-color: var(--grey-11);
--bg-modal-content-color: var(--white-color);
--bg-code-color: var(--grey-15);
--bg-navtabs-color: var(--white-color);
--bg-nav-container-color: var(--ui-gray-2);
--bg-navtabs-hover-color: var(--grey-16);
--bg-nav-tab-active-color: var(--ui-gray-4);
--bg-table-selected-color: var(--grey-14);
--bg-codemirror-color: var(--white-color);
--bg-codemirror-gutters-color: var(--grey-17);
--bg-dropdown-menu-color: var(--white-color);
--bg-log-viewer-color: var(--white-color);
@@ -129,20 +121,20 @@ html {
--bg-pre-color: var(--grey-14);
--bg-blocklist-item-selected-color: var(--grey-12);
--bg-progress-color: var(--grey-14);
--bg-pagination-color: var(--white-color);
--bg-pagination-color: var(--ui-blue-3);
--border-pagination-color: var(--ui-white);
--bg-pagination-span-color: var(--white-color);
--bg-pagination-hover-color: var(--grey-11);
--bg-pagination-hover-color: var(--ui-blue-3);
--bg-ui-select-hover-color: var(--grey-14);
--bg-motd-body-color: var(--grey-20);
--bg-item-highlighted-color: var(--grey-21);
--bg-item-highlighted-null-color: var(--grey-14);
--bg-row-header-color: var(--white-color);
--bg-image-multiselect-button: linear-gradient(var(--white-color), var(--grey-17));
--bg-multiselect-checkbox-color: var(--white-color);
--bg-sidebar-wrapper-color: var(--blue-5);
--bg-panel-body-color: var(--white-color);
--bg-codemirror-color: var(--white-color);
--bg-codemirror-selected-color: var(--grey-22);
--bg-tooltip-color: var(--ui-gray-11);
--bg-input-sm-color: var(--white-color);
--bg-app-datatable-thead: var(--grey-23);
--bg-app-datatable-tbody: var(--grey-24);
--bg-multiselect-color: var(--white-color);
--bg-daterangepicker-color: var(--white-color);
--bg-calendar-color: var(--white-color);
@@ -151,34 +143,41 @@ html {
--bg-daterangepicker-hover: var(--grey-16);
--bg-daterangepicker-in-range: var(--grey-58);
--bg-daterangepicker-active: var(--blue-14);
--bg-tooltip-color: var(--white-color);
--bg-input-autofill-color: var(--white-color);
--bg-btn-default-hover-color: var(--grey-59);
--bg-btn-default-hover-color: var(--ui-blue-9);
--bg-btn-focus: var(--grey-59);
--bg-boxselector-disabled-color: var(--white-color);
--bg-small-select-color: var(--white-color);
--bg-app-datatable-thead: var(--grey-23);
--bg-app-datatable-tbody: var(--grey-24);
--bg-stepper-item-active: var(--white-color);
--bg-stepper-item-counter: var(--grey-61);
--bg-sortbutton-color: var(--white-color);
--bg-dashboard-item: var(--ui-blue-3);
--bg-searchbar: var(--ui-gray-2);
--bg-inputbox: var(--ui-gray-2);
--bg-dropdown-hover: var(--ui-gray-3);
--bg-webeditor-color: var(--ui-gray-3);
--bg-button-group-color: var(--ui-white);
--bg-pagination-disabled-color: var(--ui-white);
--bg-code-script-color: var(--ui-white);
--bg-stepper-color: var(--ui-white);
--bg-stepper-active-color: var(--ui-blue-1);
--bg-code-color: var(--ui-white);
--text-main-color: var(--grey-7);
--text-body-color: var(--grey-6);
--text-sidebar-title-color: var(--blue-3);
--text-widget-header-color: var(--grey-7);
--text-widget-header-color: var(--ui-gray-11);
--text-form-control-color: var(--grey-25);
--text-muted-color: var(--grey-26);
--text-link-color: var(--blue-2);
--text-link-hover-color: var(--blue-4);
--text-input-group-addon-color: var(--grey-25);
--text-btn-default-color: var(--grey-6);
--text-blocklist-hover-color: var(--grey-37);
--text-dashboard-item-color: var(--grey-32);
--text-danger-color: var(--red-1);
--text-code-color: var(--red-2);
--text-navtabs-color: var(--grey-25);
--text-form-section-title-color: var(--grey-26);
--text-code-color: var(--ui-gray-9);
--text-navtabs-color: var(--grey-7);
--text-navtabs-hover-color: var(--grey-6);
--text-nav-tab-active-color: var(--grey-25);
--text-cm-default-color: var(--blue-1);
--text-cm-meta-color: var(--black-color);
--text-cm-string-color: var(--red-3);
@@ -193,27 +192,29 @@ html {
--text-blocklist-item-selected-color: var(--grey-37);
--text-progress-bar-color: var(--grey-27);
--text-pagination-color: var(--grey-26);
--text-pagination-span-color: var(--blue-2);
--text-pagination-span-hover-color: var(--blue-4);
--text-pagination-span-color: var(--grey-3);
--text-pagination-span-hover-color: var(--grey-3);
--text-ui-select-color: var(--grey-6);
--text-ui-select-hover-color: var(--grey-28);
--text-summary-color: var(--black-color);
--text-multiselect-button-color: var(--grey-29);
--text-multiselect-item-color: var(--grey-30);
--text-sidebar-list-color: var(--grey-56);
--text-tooltip-color: var(--white-color);
--text-rzslider-color: var(--grey-36);
--text-rzslider-limit-color: var(--grey-36);
--text-daterangepicker-end-date: var(--grey-57);
--text-daterangepicker-in-range: var(--black-color);
--text-daterangepicker-active: var(--white-color);
--text-tooltip-color: var(--grey-6);
--text-input-autofill-color: var(--black-color);
--text-button-hover-color: var(--grey-6);
--text-small-select-color: var(--grey-25);
--text-bootbox: var(--ui-gray-7);
--text-button-group-color: var(--ui-gray-9);
--text-button-dangerlight-color: var(--ui-error-5);
--text-stepper-active-color: var(--ui-blue-8);
--text-boxselector-header: var(--ui-black);
--border-color: var(--grey-42);
--border-widget-color: var(--grey-43);
--border-sidebar-color: var(--white-color);
--border-sidebar-color: var(--ui-blue-9);
--border-form-control-color: var(--grey-44);
--border-table-color: var(--grey-19);
--border-table-top-color: var(--grey-19);
@@ -224,35 +225,33 @@ html {
--border-boxselector-color: var(--grey-6);
--border-md-checkbox-color: var(--grey-19);
--border-modal-header-color: var(--grey-45);
--border-navtabs-color: var(--grey-19);
--border-form-section-title-color: var(--grey-26);
--border-navtabs-color: var(--ui-white);
--border-codemirror-cursor-color: var(--black-color);
--border-codemirror-gutters-color: var(--grey-19);
--border-pre-color: var(--grey-43);
--border-blocklist-item-selected-color: var(--grey-46);
--border-pagination-color: var(--grey-19);
--border-pagination-span-color: var(--grey-19);
--border-pagination-hover-color: var(--grey-19);
--border-multiselect-button-color: var(--grey-48);
--border-searchbar-color: var(--grey-10);
--border-pagination-span-color: var(--ui-white);
--border-pagination-hover-color: var(--ui-white);
--border-panel-color: var(--white-color);
--border-input-sm-color: var(--grey-47);
--border-daterangepicker-color: var(--grey-19);
--border-calendar-table: var(--white-color);
--border-daterangepicker: var(--grey-19);
--border-pre-next-month: var(--black-color);
--border-daterangepicker-after: var(--white-color);
--border-tooltip-color: var(--grey-47);
--border-modal: 0px;
--border-sortbutton: var(--grey-8);
--border-bootbox: var(--ui-gray-5);
--border-blocklist: var(--ui-gray-5);
--border-widget: var(--ui-gray-5);
--border-nav-container-color: var(--ui-gray-5);
--border-stepper-color: var(--ui-gray-4);
--hover-sidebar-color: var(--grey-37);
--shadow-box-color: 0 3px 10px -2px var(--grey-50);
--shadow-boxselector-color: 0 3px 10px -2px var(--grey-50);
--blue-color: var(--blue-13);
--button-close-color: var(--black-color);
--button-opacity: 0.2;
--button-opacity-hover: 0.5;
--bg-boxselector-wrapper-color: var(--grey-6);
--bg-image-multiselect: linear-gradient(var(--blue-2), var(--blue-2));
--bg-image-multiselect-button: linear-gradient(var(--white-color), var(--grey-17));
@@ -265,95 +264,112 @@ html {
--text-multiselect-item: var(--grey-30);
--bg-multiselect-helpercontainer: var(--white-color);
--text-input-textarea: var(--white-color);
--bg-service-datatable-thead: var(--grey-23);
--bg-inner-datatable-thead: var(--grey-23);
--bg-service-datatable-tbody: var(--grey-24);
--user-menu-icon-color: var(--ui-gray-4);
--sort-icon-muted: var(--ui-gray-5);
--sort-icon-hover: var(--ui-gray-6);
--sort-icon: var(--ui-gray-9);
--border-checkbox: var(--ui-gray-5);
--bg-checkbox: var(--white-color);
--border-searchbar: var(--grey-44);
--bg-button-group: var(--white-color);
--border-button-group: var(--ui-gray-5);
--text-button-group: var(--ui-gray-9);
}
:root[theme='dark'] {
--bg-card-color: var(--grey-1);
--bg-main-color: var(--grey-2);
/* Dark Theme */
[theme='dark'] {
--bg-body-color: var(--grey-2);
--bg-btn-default-color: var(--grey-3);
--bg-blocklist-hover-color: var(--ui-gray-iron-10);
--bg-boxselector-color: var(--ui-gray-iron-10);
--bg-blocklist-item-selected-color: var(--grey-3);
--bg-card-color: var(--grey-1);
--bg-checkbox-border-color: var(--grey-8);
--bg-sidebar-color: var(--grey-3);
--bg-code-color: var(--grey-2);
--bg-codemirror-color: var(--grey-2);
--bg-codemirror-gutters-color: var(--grey-3);
--bg-codemirror-selected-color: var(--grey-3);
--bg-dropdown-menu-color: var(--ui-gray-7);
--bg-main-color: var(--grey-2);
--bg-sidebar-color: var(--grey-1);
--bg-sidebar-nav-color: var(--grey-2);
--bg-widget-color: var(--grey-1);
--bg-widget-header-color: var(--grey-1);
--bg-widget-table-color: var(--grey-1);
--bg-widget-header-color: var(--grey-3);
--bg-widget-table-color: var(--grey-3);
--bg-header-color: var(--grey-2);
--bg-hover-table-color: var(--grey-3);
--bg-switch-box-color: var(--grey-53);
--bg-input-group-addon-color: var(--grey-3);
--bg-btn-default-color: var(--grey-3);
--bg-blocklist-hover-color: var(--grey-3);
--bg-boxselector-color: var(--grey-54);
--bg-table-color: var(--grey-1);
--bg-md-checkbox-color: var(--grey-31);
--bg-form-control-disabled-color: var(--grey-3);
--bg-modal-content-color: var(--grey-1);
--bg-code-color: var(--red-4);
--bg-navtabs-color: var(--grey-3);
--bg-nav-container-color: var(--ui-gray-iron-10);
--bg-navtabs-hover-color: var(--grey-3);
--bg-nav-tab-active-color: var(--grey-2);
--bg-table-selected-color: var(--grey-3);
--bg-codemirror-color: var(--grey-2);
--bg-codemirror-gutters-color: var(--grey-2);
--bg-dropdown-menu-color: var(--grey-1);
--bg-log-viewer-color: var(--grey-2);
--bg-log-line-selected-color: var(--grey-3);
--bg-pre-color: var(--grey-2);
--bg-blocklist-item-selected-color: var(--grey-3);
--bg-progress-color: var(--grey-3);
--bg-pagination-color: var(--grey-3);
--bg-pagination-span-color: var(--grey-3);
--bg-pagination-hover-color: var(--grey-4);
--bg-pagination-span-color: var(--grey-1);
--bg-pagination-hover-color: var(--grey-3);
--bg-ui-select-hover-color: var(--grey-3);
--bg-motd-body-color: var(--grey-1);
--bg-item-highlighted-color: var(--grey-2);
--bg-item-highlighted-null-color: var(--grey-2);
--bg-row-header-color: var(--grey-2);
--bg-multiselect-button-color: var(--grey-3);
--bg-image-multiselect-button: none !important;
--bg-multiselect-checkbox-color: var(--grey-3);
--bg-sidebar-wrapper-color: var(--grey-1);
--bg-panel-body-color: var(--grey-1);
--bg-boxselector-wrapper-disabled-color: var(--grey-39);
--bg-codemirror-selected-color: var(--grey-3);
--bg-sidebar-header-color: var(--grey-1);
--bg-input-group-addon-color: var(--grey-3);
--bg-tooltip-color: var(--grey-3);
--bg-input-sm-color: var(--grey-1);
--bg-service-datatable-thead: var(--grey-1);
--bg-inner-datatable-thead: var(--grey-1);
--bg-app-datatable-thead: var(--grey-1);
--bg-service-datatable-tbody: var(--grey-1);
--bg-app-datatable-tbody: var(--grey-1);
--bg-multiselect-color: var(--grey-1);
--bg-daterangepicker-color: var(--grey-3);
--bg-calendar-color: var(--grey-3);
--bg-calendar-table-color: var(--grey-3);
--bg-daterangepicker-end-date: var(--grey-4);
--bg-daterangepicker-hover: var(--grey-4);
--bg-daterangepicker-in-range: var(--grey-2);
--bg-daterangepicker-in-range: var(--ui-gray-warm-11);
--bg-daterangepicker-active: var(--blue-14);
--bg-tooltip-color: var(--grey-3);
--bg-input-autofill-color: var(--grey-2);
--bg-btn-default-hover-color: var(--grey-3);
--bg-btn-default-hover-color: var(--grey-4);
--bg-btn-focus: var(--grey-3);
--bg-boxselector-disabled-color: var(--grey-54);
--bg-small-select-color: var(--grey-2);
--bg-app-datatable-thead: var(--grey-1);
--bg-app-datatable-tbody: var(--grey-1);
--bg-stepper-item-active: var(--grey-1);
--bg-stepper-item-counter: var(--grey-7);
--bg-sortbutton-color: var(--grey-1);
--bg-dashboard-item: var(--grey-3);
--bg-searchbar: var(--ui-grey-warm-11);
--bg-inputbox: var(--grey-2);
--bg-dropdown-hover: var(--grey-3);
--bg-webeditor-color: var(--ui-gray-iron-10);
--bg-button-group-color: var(--ui-black);
--bg-pagination-disabled-color: var(--grey-1);
--bg-code-script-color: var(--ui-gray-warm-11);
--bg-stepper-color: var(--ui-gray-iron-10);
--bg-stepper-active-color: var(--ui-blue-8);
--text-main-color: var(--white-color);
--text-body-color: var(--white-color);
--text-sidebar-title-color: var(--grey-8);
--text-widget-header-color: var(--white-color);
--text-form-control-color: var(--white-color);
--text-muted-color: var(--grey-8);
--text-link-color: var(--blue-9);
--text-link-hover-color: var(--blue-2);
--text-input-group-addon-color: var(--grey-8);
--text-btn-default-color: var(--grey-8);
--text-blocklist-hover-color: var(--white-color);
--text-dashboard-item-color: var(--blue-2);
--text-danger-color: var(--red-4);
--text-code-color: var(--white-color);
--text-navtabs-color: var(--white-color);
--text-form-section-title-color: var(--grey-8);
--text-navtabs-color: var(--grey-8);
--text-navtabs-hover-color: var(--grey-9);
--text-nav-tab-active-color: var(--white-color);
--text-cm-default-color: var(--blue-10);
--text-cm-meta-color: var(--white-color);
--text-cm-string-color: var(--red-5);
@@ -368,27 +384,30 @@ html {
--text-blocklist-item-selected-color: var(--white-color);
--text-progress-bar-color: var(--white-color);
--text-pagination-color: var(--white-color);
--text-pagination-span-color: var(--blue-2);
--text-pagination-span-hover-color: var(--white-color);
--text-pagination-span-color: var(--ui-white);
--text-pagination-span-hover-color: var(--ui-white);
--text-ui-select-color: var(--white-color);
--text-ui-select-hover-color: var(--white-color);
--text-summary-color: var(--white-color);
--text-multiselect-button-color: var(--white-color);
--text-multiselect-item-color: var(--white-color);
--text-sidebar-list-color: var(--white-color);
--text-boxselector-wrapper-color: var(--white-color);
--text-tooltip-color: var(--white-color);
--text-rzslider-color: var(--white-color);
--text-rzslider-limit-color: var(--white-color);
--text-daterangepicker-end-date: var(--grey-7);
--text-daterangepicker-in-range: var(--white-color);
--text-daterangepicker-active: var(--white-color);
--text-tooltip-color: var(--white-color);
--text-btn-default-color: var(--white-color);
--text-input-autofill-color: var(--grey-8);
--text-button-hover-color: var(--white-color);
--text-small-select-color: var(--grey-7);
--text-bootbox: var(--white-color);
--text-button-group-color: var(--ui-white);
--text-button-dangerlight-color: var(--ui-error-7);
--text-stepper-active-color: var(--ui-white);
--text-boxselector-header: var(--ui-white);
--border-color: var(--grey-3);
--border-widget-color: var(--grey-1);
--border-sidebar-color: var(--blue-9);
--border-sidebar-color: var(--ui-gray-8);
--border-form-control-color: var(--grey-54);
--border-table-color: var(--grey-3);
--border-table-top-color: var(--grey-3);
@@ -400,28 +419,27 @@ html {
--border-md-checkbox-color: var(--grey-41);
--border-modal-header-color: var(--grey-1);
--border-navtabs-color: var(--grey-38);
--border-form-section-title-color: var(--grey-8);
--border-codemirror-cursor-color: var(--white-color);
--border-codemirror-gutters-color: var(--grey-26);
--border-pre-color: var(--grey-3);
--border-blocklist-item-selected-color: var(--grey-38);
--border-pagination-color: var(--grey-3);
--border-pagination-span-color: var(--grey-3);
--border-pagination-span-color: var(--grey-1);
--border-pagination-hover-color: var(--grey-3);
--border-pagination-hover-color: var(--grey-3);
--border-multiselect-button-color: var(--grey-3);
--border-searchbar-color: var(--grey-1);
--border-panel-color: var(--grey-2);
--border-input-sm-color: var(--grey-3);
--border-daterangepicker-color: var(--grey-3);
--border-calendar-table: var(--grey-3);
--border-daterangepicker: var(--grey-4);
--border-pre-next-month: var(--white-color);
--border-daterangepicker-after: var(--grey-3);
--border-tooltip-color: var(--grey-3);
--border-modal: 0px;
--border-sortbutton: var(--grey-3);
--border-bootbox: var(--ui-gray-9);
--border-blocklist: var(--ui-gray-9);
--border-widget: var(--grey-3);
--border-pagination-color: var(--grey-1);
--border-nav-container-color: var(--ui-gray-neutral-8);
--border-stepper-color: var(--ui-gray-warm-9);
--hover-sidebar-color: var(--grey-3);
--blue-color: var(--blue-2);
--button-close-color: var(--white-color);
--button-opacity: 0.6;
@@ -439,17 +457,27 @@ html {
--text-multiselect-item: var(--white-color);
--bg-multiselect-helpercontainer: var(--grey-1);
--text-input-textarea: var(--grey-1);
--bg-service-datatable-thead: var(--grey-1);
--bg-inner-datatable-thead: var(--grey-1);
--bg-service-datatable-tbody: var(--grey-1);
--user-menu-icon-color: var(--grey-3);
--sort-icon-muted: var(--ui-gray-7);
--sort-icon-hover: var(--ui-gray-6);
--sort-icon: var(--ui-gray-3);
--border-checkbox: var(--ui-gray-5);
--bg-checkbox: var(--white-color);
--border-searchbar: var(--grey-54);
--bg-button-group: var(--white-color);
--border-button-group: var(--ui-gray-5);
--text-button-group: var(--ui-gray-9);
}
:root[theme='highcontrast'] {
/* High Contrast Theme */
[theme='highcontrast'] {
--bg-card-color: var(--black-color);
--bg-main-color: var(--black-color);
--bg-body-color: var(--black-color);
--bg-checkbox-border-color: var(--grey-8);
--bg-sidebar-color: var(--black-color);
--bg-sidebar-nav-color: var(--black-color);
--bg-widget-color: var(--black-color);
--bg-widget-header-color: var(--black-color);
--bg-widget-table-color: var(--black-color);
@@ -457,31 +485,29 @@ html {
--bg-hover-table-color: var(--grey-3);
--bg-switch-box-color: var(--grey-53);
--bg-panel-body-color: var(--black-color);
--bg-boxselector-wrapper-disabled-color: var(--grey-39);
--bg-dropdown-menu-color: var(--black-color);
--bg-codemirror-selected-color: var(--grey-3);
--bg-row-header-color: var(--black-color);
--bg-sidebar-wrapper-color: var(--black-color);
--bg-motd-body-color: var(--black-color);
--bg-blocklist-hover-color: var(--black-color);
--bg-blocklist-item-selected-color: var(--black-color);
--bg-input-group-addon-color: var(--grey-1);
--bg-input-group-addon-color: var(--grey-3);
--bg-table-color: var(--black-color);
--bg-codemirror-gutters-color: var(--black-color);
--bg-codemirror-gutters-color: var(--ui-gray-warm-11);
--bg-codemirror-color: var(--black-color);
--bg-codemirror-selected-color: var(--grey-3);
--bg-log-viewer-color: var(--black-color);
--bg-log-line-selected-color: var(--grey-3);
--bg-sidebar-header-color: var(--black-color);
--bg-modal-content-color: var(--black-color);
--bg-form-control-disabled-color: var(--grey-1);
--bg-input-sm-color: var(--black-color);
--bg-item-highlighted-color: var(--black-color);
--bg-service-datatable-thead: var(--black-color);
--bg-inner-datatable-thead: var(--black-color);
--bg-app-datatable-thead: var(--black-color);
--bg-service-datatable-tbody: var(--black-color);
--bg-app-datatable-tbody: var(--black-color);
--bg-pagination-color: var(--grey-3);
--bg-pagination-span-color: var(--grey-3);
--bg-pagination-span-color: var(--ui-black);
--bg-multiselect-color: var(--grey-1);
--bg-daterangepicker-color: var(--black-color);
--bg-calendar-color: var(--black-color);
@@ -493,29 +519,32 @@ html {
--bg-tooltip-color: var(--black-color);
--bg-table-selected-color: var(--grey-3);
--bg-pre-color: var(--grey-2);
--bg-nav-container-color: var(--ui-black);
--bg-navtabs-hover-color: var(--grey-3);
--bg-nav-tab-active-color: var(--ui-black);
--bg-btn-default-color: var(--black-color);
--bg-code-color: var(--red-4);
--bg-navtabs-color: var(--black-color);
--bg-input-autofill-color: var(--black-color);
--bg-code-color: var(--grey-2);
--bg-navtabs-color: var(--grey-2);
--bg-navtabs-hover-color: var(--grey-3);
--bg-btn-default-hover-color: var(--grey-3);
--bg-btn-default-color: var(--black-color);
--bg-code-color: var(--ui-black);
--bg-btn-default-hover-color: var(--grey-4);
--bg-btn-focus: var(--black-color);
--bg-boxselector-color: var(--black-color);
--bg-boxselector-disabled-color: var(--black-color);
--bg-small-select-color: var(--black-color);
--bg-app-datatable-thead: var(--black-color);
--bg-app-datatable-tbody: var(--black-color);
--bg-stepper-item-active: var(--black-color);
--bg-stepper-item-counter: var(--grey-3);
--bg-sortbutton-color: var(--grey-1);
--bg-inputbox: var(--black-color);
--bg-searchbar: var(--black-color);
--bg-dropdown-hover: var(--black-color);
--bg-webeditor-color: var(--ui-gray-warm-9);
--bg-pagination-disabled-color: var(--ui-black);
--bg-pagination-hover-color: var(--ui-black);
--bg-code-script-color: var(--ui-black);
--bg-stepper-active-color: var(--ui-blue-8);
--bg-stepper-color: var(--ui-black);
--text-main-color: var(--white-color);
--text-body-color: var(--white-color);
--text-sidebar-title-color: var(--grey-8);
--text-widget-header-color: var(--white-color);
--text-link-color: var(--blue-9);
--text-link-hover-color: var(--blue-9);
@@ -525,7 +554,6 @@ html {
--text-blocklist-hover-color: var(--blue-11);
--text-boxselector-wrapper-color: var(--white-color);
--text-dashboard-item-color: var(--blue-12);
--text-form-section-title-color: var(--white-color);
--text-muted-color: var(--white-color);
--text-tooltip-color: var(--white-color);
--text-blocklist-item-selected-color: var(--blue-9);
@@ -537,56 +565,57 @@ html {
--text-rzslider-color: var(--white-color);
--text-rzslider-limit-color: var(--white-color);
--text-pagination-color: var(--white-color);
--text-daterangepicker-end-date: var(--grey-7);
--text-daterangepicker-end-date: var(--ui-white);
--text-daterangepicker-in-range: var(--white-color);
--text-daterangepicker-active: var(--white-color);
--text-sidebar-list-color: var(--white-color);
--text-ui-select-color: var(--white-color);
--text-btn-default-color: var(--white-color);
--text-json-tree-color: var(--white-color);
--text-json-tree-leaf-color: var(--white-color);
--text-json-tree-branch-preview-color: var(--white-color);
--text-pre-color: var(--white-color);
--text-navtabs-color: var(--white-color);
--text-navtabs-hover-color: var(--white-color);
--text-nav-tab-active-color: var(--white-color);
--text-input-autofill-color: var(--white-color);
--text-navtabs-color: var(--white-color);
--text-button-hover-color: var(--white-color);
--text-btn-default-color: var(--white-color);
--text-small-select-color: var(--white-color);
--text-multiselect-item-color: var(--white-color);
--text-pagination-span-color: var(--blue-2);
--text-pagination-span-color: var(--ui-white);
--text-bootbox: var(--white-color);
--text-pagination-span-hover-color: var(--ui-white);
--text-stepper-active-color: var(--ui-white);
--text-boxselector-header: var(--ui-white);
--border-color: var(--grey-55);
--border-widget-color: var(--white-color);
--border-sidebar-color: var(--blue-9);
--border-sidebar-color: var(--white-color);
--border-form-control-color: var(--grey-54);
--border-table-color: var(--grey-55);
--border-table-top-color: var(--grey-55);
--border-datatable-top-color: var(--grey-55);
--border-sidebar-high-contrast: 1px solid var(--blue-9);
--border-code-high-contrast: 1px solid var(--white-color);
--border-boxselector-wrapper: 3px solid var(--blue-2);
--border-boxselector-wrapper-hover: 3px solid var(--blue-8);
--border-panel-color: var(--white-color);
--border-input-group-addon-color: var(--grey-54);
--border-modal-header-color: var(--grey-3);
--border-input-sm-color: var(--white-color);
--border-pagination-color: var(--grey-3);
--border-pagination-span-color: var(--grey-3);
--border-pagination-color: var(--grey-1);
--border-pagination-span-color: var(--grey-1);
--border-daterangepicker-color: var(--white-color);
--border-calendar-table: var(--black-color);
--border-daterangepicker: var(--black-color);
--border-pre-next-month: var(--white-color);
--border-daterangepicker-after: var(--black-color);
--border-tooltip-color: var(--white-color);
--border-pre-color: var(--grey-3);
--border-codemirror-cursor-color: var(--white-color);
--border-modal: 1px solid var(--white-color);
--border-blocklist-color: var(--white-color);
--border-sortbutton: var(--black-color);
--border-bootbox: var(--black-color);
--border-blocklist: var(--white-color);
--border-widget: var(--white-color);
--border-nav-container-color: var(--ui-white);
--border-stepper-color: var(--ui-gray-warm-9);
--hover-sidebar-color: var(--blue-9);
--hover-sidebar-color: var(--black-color);
--shadow-box-color: none;
--shadow-boxselector-color: none;
@@ -605,4 +634,15 @@ html {
--text-cm-meta-color: var(--white-color);
--text-cm-string-color: var(--red-7);
--text-progress-bar-color: var(--black-color);
--user-menu-icon-color: var(--white-color);
--sort-icon-muted: var(--ui-gray-7);
--sort-icon-hover: var(--ui-gray-6);
--sort-icon: var(--ui-gray-3);
--border-checkbox: var(--ui-gray-5);
--bg-checkbox: var(--white-color);
--border-searchbar: var(--ui-gray-5);
--bg-button-group: var(--white-color);
--border-button-group: var(--ui-gray-5);
--text-button-group: var(--ui-gray-9);
}
+106 -44
View File
@@ -1,7 +1,7 @@
/* Overide Vendor CSS */
.form-control {
background-color: var(--bg-main-color) !important;
border: 1px solid var(--border-form-control-color);
background-color: var(--bg-inputbox);
color: var(--text-form-control-color);
}
@@ -10,7 +10,7 @@
}
.table > thead > tr > th {
border-bottom: 2px solid var(--border-table-color);
border-bottom: 1px solid var(--border-table-color);
}
.table-hover > tbody > tr:hover {
@@ -31,33 +31,18 @@
border-top: 1px solid var(--border-table-top-color);
}
a {
color: var(--text-link-color);
}
a:hover,
a:focus {
color: var(--text-link-hover-color);
}
.input-group-addon {
color: var(--text-input-group-addon-color);
background-color: var(--bg-input-group-addon-color);
border: 1px solid var(--border-input-group-addon-color);
}
.btn-default {
color: var(--text-btn-default-color);
background-color: var(--bg-btn-default-color);
border-color: var(--border-btn-default-color);
}
.text-danger {
color: var(--text-danger-color);
color: var(--ui-error-9);
}
.table .table {
background-color: var(--bg-table-color);
background-color: initial;
}
.table-bordered {
@@ -119,8 +104,8 @@ code {
.nav-tabs > li.active > a,
.nav-tabs > li.active > a:hover,
.nav-tabs > li.active > a:focus {
color: var(--text-navtabs-color);
background-color: var(--bg-navtabs-color);
color: var(--text-nav-tab-active-color);
background-color: var(--bg-nav-tab-active-color);
border: 1px solid var(--border-navtabs-color);
}
@@ -132,8 +117,13 @@ code {
border-color: var(--border-navtabs-color);
}
.nav > li > a {
color: var(--text-navtabs-color);
}
.nav > li > a:hover,
.nav > li > a:focus {
color: var(--text-navtabs-hover-color);
background-color: var(--bg-navtabs-hover-color);
}
@@ -161,12 +151,22 @@ code {
.CodeMirror-gutters {
background: var(--bg-codemirror-gutters-color);
border-right: 1px solid var(--border-codemirror-gutters-color);
border-right: 0px;
}
.CodeMirror-linenumber {
text-align: left;
}
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like {
padding: 0 20px;
}
.CodeMirror {
background: var(--bg-codemirror-color);
color: var(--text-codemirror-color);
border-radius: 8px;
}
.CodeMirror-selected {
@@ -195,6 +195,7 @@ code {
.dropdown-menu {
background: var(--bg-dropdown-menu-color);
border-radius: 8px;
}
.dropdown-menu > li > a {
@@ -203,6 +204,7 @@ code {
pre {
border: 1px solid var(--border-pre-color);
border-radius: 8px;
background-color: var(--bg-pre-color);
color: var(--text-pre-color);
}
@@ -222,6 +224,27 @@ json-tree .branch-preview {
background-color: var(--bg-progress-color);
}
.ui-select-search,
.ui-select-toggle {
height: 30px;
min-width: 260px;
padding: 4px 12px;
}
.ui-select-toggle {
justify-content: flex-start !important;
}
.ui-select-match-text {
display: flex;
flex-direction: row-reverse;
align-items: center;
}
.ui-select-match-text > a {
verical-align: middle;
}
.ui-select-bootstrap .ui-select-choices-row > span {
color: var(--text-ui-select-color);
}
@@ -243,6 +266,10 @@ json-tree .branch-preview {
.panel {
border: 1px solid var(--border-panel-color);
background-color: var(--bg-panel-body-color);
border-radius: 8px;
-webkit-box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05);
-moz-box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05);
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.05);
}
.theme-information .col-sm-12 {
@@ -272,8 +299,15 @@ json-tree .branch-preview {
.rzslider .rz-bubble.rz-limit {
color: var(--text-rzslider-limit-color);
}
.rz-bubble.rz-limit.rz-ceil {
position: absolute;
right: 0;
left: auto !important;
top: -26px;
}
input,
button,
select,
textarea {
background: var(--text-input-textarea);
@@ -353,31 +387,59 @@ input:-webkit-autofill {
-webkit-text-fill-color: var(--text-input-autofill-color) !important;
}
.btn:hover {
color: var(--text-button-hover-color);
}
.btn-default:hover {
background-color: var(--bg-btn-default-hover-color);
}
.btn-primary:hover {
color: var(--white-color) !important;
}
.btn-danger:hover {
color: var(--white-color);
}
/* Overide Vendor CSS */
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn {
pointer-events: none;
touch-action: none;
.btn-link:hover {
color: var(--text-link-hover-color) !important;
}
.multiSelect.inlineBlock button {
margin: 0;
}
.nav-tabs > li.active > a {
border: 0px;
}
.label-default {
line-height: 11px;
}
/* Code Script Style */
.code-script {
background-color: var(--bg-code-script-color);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
padding: 5px;
}
.nav-container {
border: 1px solid var(--border-nav-container-color);
background-color: var(--bg-nav-container-color);
border-radius: 8px;
padding: 10px;
}
.nav-tabs > li {
background-color: var(--bg-nav-tabs-active-color);
border-top-right-radius: 8px;
}
/* Code Script Style */
.code-script {
background-color: var(--bg-code-script-color);
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
padding: 5px;
}
.nav-container {
border: 1px solid var(--border-nav-container-color);
background-color: var(--bg-nav-container-color);
border-radius: 8px;
padding: 10px;
}
.nav-tabs > li {
border-top-right-radius: 8px;
}
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.
Binary file not shown.

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