Compare commits

...

83 Commits

Author SHA1 Message Date
yi-portainer
4cb6bb863e Merge branch 'release/2.9' 2021-11-17 15:00:33 +13:00
Matt Hook
4d906e0d42 fix(dockerhub-migration): prevent duplicate migrated dockerhub entries EE-2042 (#6084)
* add missing changes to make updateDockerhubToDB32 idempotent

* fix(migration) make dockerhub registry migration idempotent EE-2042

* add tests for bad migrations
2021-11-17 13:20:28 +13:00
Matt Hook
05041fe7fd update version to 2.9.3 2021-11-01 13:16:15 +13:00
Matt Hook
1ea9b421e0 update version to 2.9.3 2021-11-01 13:09:10 +13:00
Matt Hook
a5a7e2c868 fix(migration): bubble up recovered panic in new error EE-1971 (#5998)
* fix(migration): bubble up recovered panic in new error EE-1971

* improve code and add comments
2021-10-30 22:33:06 +13:00
Hui
bb832d285b fix(migration): ignore volumes with no created timestamp EE-1966 2021-10-30 11:09:32 +13:00
Platforms Team
caced72ec1 Merge branch 'ado-release' 2021-10-26 03:57:28 +00:00
cong meng
0d72896b6b fix(image) EE-1955 unable to tag image (#5973)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-26 15:22:42 +13:00
Platforms Team
48b69852eb Merge branch 'ado-release' 2021-10-25 20:49:52 +00:00
Richard Wei
40a6645e23 fix user not able to get nodes (#5950) 2021-10-21 11:55:37 +13:00
Stéphane Busso
90a18b5ded Bump dbversion 2021-10-20 20:35:18 +13:00
Hui
d17e7c8160 fix(stack): auto update breaks after restarting Portainer EE-1915 2021-10-20 16:00:40 +13:00
Matt Hook
f0efc4f904 bump to 2.9.2 2021-10-19 15:51:16 +13:00
cong meng
4f350ab6f5 fix(registry) EE-1861 improve registry selection (#5921)
* fix(registry) EE-1861 fail to select registry with same name

* fix(registry) EE-1861 show registry modal when pull and push image

* fix(registry) EE-1861 cleanup code

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-19 14:54:44 +13:00
fhanportainer
1ff5f25e40 fix(registry): ignore pull limit in non-docker hub registry. (#5917) 2021-10-19 13:21:57 +13:00
Matt Hook
006634e007 fix(helm): allow settings to be saved offline EE-1907 (#5908)
* skip validating default helm repo to allow offline saving of settings. Default repo is hardcoded and correct.

* dont validate the helm repo if the repo hasn't changed or is the default

* fix logic
2021-10-18 15:08:38 +13:00
cong meng
9dcd5651e8 fix(registry) EE-1861 improve registry selection (#5899)
* fix(registry) EE-1861 hide anonymous dockerhub registry if user has an authenticated one

* fix(registry) EE-1861 pick up a best match dockerhub registry

* fix(registry) EE-1861 set the anonymous registry as default if it is shown

* fix(registry) EE-1861 refactor how to match registry

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-15 21:42:46 +13:00
andres-portainer
dfe0b3f69d fix(namespaces): remove the stacks from the data store when deleting their corresponding Kubernetes namespace EE-1872 (#5885)
* fix(namespaces): remove the stacks from the data store when deleting their corresponding Kubernetes namespace EE-1872

* add endpoint ID checking

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: ArrisLee <arris_li@hotmail.com>
2021-10-14 19:15:04 -03:00
cong meng
f544d4447c fix(rbac) EE-1867 regular user unable to access pod and node stats view (#5886)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-14 17:00:31 +13:00
yi-portainer
273ef6c2ed Merge branch 'release/2.9' 2021-10-11 12:39:33 +13:00
Chaim Lev-Ari
8383bc05c5 fix(compose): use tcp for agent proxy EE-1807 (#5854) 2021-10-11 12:08:07 +13:00
yi-portainer
bac7c89363 Merge branch 'release/2.9' 2021-10-11 08:05:14 +13:00
wheresolivia
0200a668df fix(ui): ldap group search config labelclose EE-1846 (#5850)
Co-authored-by: olivia.wang <olivia.wang@wherescape.com>
2021-10-08 12:01:10 +13:00
fhanportainer
dcd1e902cd fix(ldap): enable user/group setting in custom ldap (#5858) 2021-10-08 11:39:16 +13:00
zees-dev
c93ec8d08c added swagger docs to websocketShellPodExec (#5840) 2021-10-08 10:32:43 +13:00
Chaim Lev-Ari
b7841e7fc3 feat(app): highlight be provided value [EE-882] (#5703) (#5835) 2021-10-07 11:59:53 +13:00
Matt Hook
8096c5e8bc remove default value for compose path (#5832)
Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
2021-10-07 08:07:00 +13:00
Stéphane Busso
551d287982 Merge branch 'release/2.9' of github.com:portainer/portainer into release/2.9 2021-10-02 09:26:23 +13:00
Chaim Lev-Ari
885ae16278 fix(db): warn on missing docker id when migrating to db 31 (#5782)
* fix(db): warn on missing docker id when migrating to db 31

* fix(db): guard against nil exception
2021-10-01 15:27:31 +10:00
Chaim Lev-Ari
9c279e7fae fix(k8s/ns): validate ingress ctrl host pattern (#5662)
* fix(k8s/ns): validate ingress ctrl host pattern

* feat(kube/ns): validate ingress hostname
2021-09-24 14:02:10 +03:00
waysonwei
4bdf3ecf58 fix decl.moveTo is not a function error in css 2021-09-23 14:15:12 +12:00
yi-portainer
89dc83f24a * sync with release/2.9 2021-09-23 11:21:46 +12:00
yi-portainer
4af6dcea0e Merge branch 'release/2.9' 2021-09-23 10:54:30 +12:00
yi-portainer
d369a71ceb Merge branch 'release/2.6' 2021-08-27 09:40:19 +12:00
Stéphane Busso
1fb5d31f7e Bump to 2.6.3 2021-08-27 09:25:49 +12:00
LP B
9c616ffb07 feat(app/k8s): update ingress scheme from v1beta1 to v1 (#5466) 2021-08-25 18:35:03 +12:00
yi-portainer
dbae99ea87 Merge branch 'release/2.6' 2021-07-30 11:14:07 +12:00
yi-portainer
3254051647 * update version to 2.6.2 2021-07-30 10:28:09 +12:00
yi-portainer
f0d128f212 Merge branch 'release/2.6' 2021-07-29 17:37:27 +12:00
Matt Hook
a0b52fc3d7 Fixes for EE-1035 and dockerhub pro accounts. (#5343) 2021-07-27 10:41:58 +12:00
cong meng
31fdef1e60 fix(advance deploy): EE-1141 A standard user can escalate to cluster administrator privileges on Kubernetes (#5324)
* fix(advance deploy): EE-1141 A standard user can escalate to cluster administrator privileges on Kubernetes

* fix(advance deploy): EE-1141 reuse existing token cache when do deployment

* fix: EE-1141 use user's SA token to exec pod command

* fix: EE-1141 stop advanced-deploy or pod-exec if user's SA token is empty

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-07-27 09:55:09 +12:00
Hui
be30e1c453 fix(swagger): add swagger annotation for pull and redeploy stack 2021-07-22 11:39:47 +12:00
Richard Wei
5b55b890e7 fix charts x label padding (#5339) 2021-07-21 13:54:26 +12:00
Dmitry Salakhov
a5eac07b0c fix(namespace): update portainer-config when delete a namespace (#5328) 2021-07-20 14:05:40 +12:00
fhanportainer
fa80a7b7e5 fix(k8s): fixed generating kube auction summary issue (#5332) 2021-07-19 19:45:14 +12:00
yi-portainer
b14500a2d5 Merge branch 'release/2.6' 2021-07-09 16:43:09 +12:00
cong meng
278667825a EE-1110 Ingress routes and their mapping to a application name are not deleted when the application is deleted (#5291)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-07-09 10:39:14 +12:00
cong meng
65ded647b6 fix(ingress): fixed hostname field when having multiple ingresses EE-1072 (#5273) (#5285)
Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
2021-07-08 12:08:20 +12:00
Richard Wei
084cdcd8dc fix(app):Set resource assignment default to off EE-1043 (#5286) 2021-07-08 12:08:10 +12:00
Stéphane Busso
5b68c4365e Merge branch 'release/2.6' of github.com:portainer/portainer into release/2.6 2021-07-08 11:39:21 +12:00
Stéphane Busso
9cd64664cc fix download logs (#5243) 2021-07-08 11:37:18 +12:00
yi-portainer
e831fa4a03 * update versions to 2.6.1 2021-07-07 17:20:18 +12:00
cong meng
2a3c807978 fix(ingress): EE-1049 Ingress config is lost when deleting an application deployed with ingress (#5264)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-07-07 14:08:20 +12:00
cong meng
a8265a44d0 fix EE-1078 Too strict form validation for docker environment variables (#5278)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-07-07 12:52:37 +12:00
Hui
71ad21598b remove expiry time copy logic (#5259) 2021-06-30 16:49:48 +12:00
yi-portainer
6e017ea64e Merge branch 'release/2.6' 2021-06-25 00:03:04 +12:00
yi-portainer
d48980e85b Merge branch 'release/2.5' 2021-05-28 10:22:50 +12:00
yi-portainer
80d3fcc40b Merge branch 'release/2.5' 2021-05-28 10:17:05 +12:00
yi-portainer
2e92706ead Merge branch 'release/2.5' 2021-05-24 08:50:46 +12:00
yi-portainer
d4fa9db432 Merge branch 'release/2.5' 2021-05-17 13:59:38 +12:00
yi-portainer
a28559777f Merge branch 'release/2.1' 2021-05-17 13:43:48 +12:00
yi-portainer
f6531627d4 Squashed commit of the following:
commit 535215833d
Author: yi-portainer <yi.chen@portainer.io>
Date:   Thu Feb 4 18:04:18 2021 +1300

    * version change to 2.1.1

commit c4a1243af9
Author: Dmitry Salakhov <to@dimasalakhov.com>
Date:   Thu Feb 4 03:00:25 2021 +0000

    fix: docker-compose use custom config.json to access private images (#4820)

commit 305d0d2da0
Author: LP B <xAt0mZ@users.noreply.github.com>
Date:   Wed Feb 3 06:38:56 2021 +0100

    fix(k8s/resource-pool): unusable RP access management (#4810)

    (cherry picked from commit e401724d43)

commit e4605d990d
Author: yi-portainer <yi.chen@portainer.io>
Date:   Tue Feb 2 17:42:57 2021 +1300

    * update portainer version

commit 768697157c
Author: LP B <xAt0mZ@users.noreply.github.com>
Date:   Tue Feb 2 05:00:19 2021 +0100

    sec(app): remove unused and vulnerable dependencies (#4801)

commit d3086da139
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Feb 2 15:10:06 2021 +1300

    fix(k8s) trigger port validation while changing protocol (ce#394) (#4804)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 95894e8047
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Feb 2 15:03:11 2021 +1300

    fix(k8s) parse empty configuration as empty string yaml instead of {} (ce#395) (#4805)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 81de55fedd
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Tue Feb 2 11:12:40 2021 +1300

    * fix missing kubectl download (#4802)

commit 84827b8782
Author: Steven Kang <skan070@gmail.com>
Date:   Sun Jan 31 17:32:30 2021 +1300

    feat(build): introducing buildx for Windows (#4792)

    * feat(build): introducing buildx for Windows

    * feat(build): re-ordered USER

    * feat(build): Fixed Typo

    * feat(build): fixed typo

commit a71e71f481
Author: Dmitry Salakhov <to@dimasalakhov.com>
Date:   Mon Jan 25 19:16:53 2021 +0000

    feat(compose): add docker-compose wrapper (#4713)

    * feat(compose): add docker-compose wrapper

    ce-187

    * fix(compose): pick compose implementation upon startup

    * Add static compose build for linux

    * Fix wget

    * Fix platofrm specific docker-compose download

    * Keep amd64 architecture as download parameter

    * Add tmp folder for docker-compose

    * fix: line endings

    * add proxy server

    * logs

    * Proxy

    * Add lite transport for compose

    * Fix local deployment

    * refactor: pass proxyManager by ref

    * fix: string conversion

    * refactor: compose wrapper remove unused code

    * fix: tests

    * Add edge

    * Fix merge issue

    * refactor: remove unused code

    * Move server to proxy implementation

    * Cleanup wrapper and manager

    * feat: pass max supported compose syntax version with each endpoint

    * fix: pick compose syntax version

    * fix: store wrapper version in portainer

    * Get and show composeSyntaxMaxVersion at stack creation screen

    * Get and show composeSyntaxMaxVersion at stack editor screen

    * refactor: proxy server

    * Fix used tmp

    * Bump docker-compose to 1.28.0

    * remove message for docker compose limitation

    * fix: markup typo

    * Rollback docker compose to 1.27.4

    * * attempt to fix the windows build issue

    * * attempt to debug grunt issue

    * * use console log in grunt file

    * fix: try to fix windows build by removing indirect deps from go.mod

    * Remove tmp folder

    * Remove builder stage

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose - fixed verbose output

    * refactor: renames

    * fix(stack): get endpoint by EndpointProvider

    * fix(stack): use margin to add space between line instead of using br tag

    Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: yi-portainer <yi.chen@portainer.io>
    Co-authored-by: Steven Kang <skan070@gmail.com>

commit 83f4c5ec0b
Author: LP B <xAt0mZ@users.noreply.github.com>
Date:   Mon Jan 25 02:43:54 2021 +0100

    fix(k8s/app): remove advanced deployment panel from app details view (#4730)

commit 41308d570d
Author: Maxime Bajeux <max.bajeux@gmail.com>
Date:   Mon Jan 25 02:14:35 2021 +0100

    feat(configurations): Review UI/UX configurations (#4691)

    * feat(configurations): Review UI/UX configurations

    * feat(configurations): fix binary secret value

    * fix(frontend): populate data between simple and advanced modes (#4503)

    * fix(configuration): parseYaml before create configuration

    * fix(configurations): change c to C in ConfigurationOwner

    * fix(application): change configuration index to configuration key in the view

    * fix(configuration): resolve problem in application create with configuration not overriden.

    * fix(configuration): fix bad import in helper

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 46ff8a01bc
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Fri Jan 22 03:08:08 2021 +0200

    fix(kubernetes/pods): save note (#4675)

    * feat(kubernetes/pods): introduce patch api

    * feat(k8s/pods): pod converter

    * feat(kubernetes/pods): introduce patch api

    * feat(k8s/pod): add annotations only if needed

    * fix(k8s/pod): replace class with factory function

commit 2b257d2785
Author: yi-portainer <yi.chen@portainer.io>
Date:   Thu Jan 21 00:02:22 2021 +1300

    Squashed commit of the following 2.0.1 release fixes:

    commit f90d6b55d6
    Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
    Date:   Wed Jan 13 00:56:19 2021 +0200

        feat(service): clear source volume when change type (#4627)

        * feat(service): clear source volume when change type

        * feat(service): init volume source to the correct value

    commit 1b82b450d7
    Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
    Date:   Thu Jan 7 14:47:32 2021 +1300

        * bump the APIVersion to 2.0.1 (#4688)

    commit b78d804881
    Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
    Date:   Wed Dec 30 23:03:43 2020 +1300

        Revert "chore(build): bump Kompose version (#4475)" (#4676)

        This reverts commit 380f106571.

        Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    commit 51b72c12f9
    Author: Anthony Lapenna <anthony.lapenna@portainer.io>
    Date:   Wed Dec 23 14:45:32 2020 +1300

        fix(docker/stack-details): do not display editor tab for external stack (#4650)

    commit 58c04bdbe3
    Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
    Date:   Tue Dec 22 13:47:11 2020 +1300

        + silently continue when downloading artifacts in windows (#4637)

    commit a6320d5222
    Author: cong meng <mcpacino@gmail.com>
    Date:   Tue Dec 22 13:38:54 2020 +1300

        fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)

        * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

        * fix(frontend) rephrase comments (#4629)

        Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

        Co-authored-by: Simon Meng <simon.meng@portainer.io>
        Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

commit da41dbb79a
Author: cong meng <mcpacino@gmail.com>
Date:   Wed Jan 20 15:19:35 2021 +1300

    fix(stack): stacks created via API are incorrectly marked as private with no owner (#3721) (#4725)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 68d42617f2
Author: Maxime Bajeux <max.bajeux@gmail.com>
Date:   Wed Jan 20 01:02:18 2021 +0100

    feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster (#4525)

    * feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster

    * fix(applications): if there is at least one node the application can schedule on, then do not show the warning

commit 8323e22309
Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com>
Date:   Wed Jan 20 12:06:25 2021 +1300

    Update issue templates

    Adding auto labelling to Bug Report (kind/bug, bug/unconfirmed) and Question (kind/question)

commit 20d4341170
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Tue Jan 19 00:10:08 2021 +0200

    fix(state): check validity of state (#4609)

commit 832cafc933
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Mon Jan 18 02:59:57 2021 +0200

    fix(registries): update password only when not empty (#4669)

commit f3c537ac2c
Author: cong meng <mcpacino@gmail.com>
Date:   Mon Jan 18 13:02:16 2021 +1300

    chore(build): bump Kompose version (#4473) (#4724)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 958baf6283
Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com>
Date:   Mon Jan 18 09:30:17 2021 +1300

    Update README.md

commit 08e392378e
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Sun Jan 17 09:28:09 2021 +0200

    chore(app): fail on angular components missing nginject (#4224)

commit a2d9734b8b
Author: Alice Groux <alice.grx@gmail.com>
Date:   Sun Jan 17 04:50:22 2021 +0100

    fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable (#4511)

    * fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable

    * fix(k8s/datatables): reduce size of expand/collapse column

commit 15aed9fc6f
Author: DarkAEther <30438425+DarkAEther@users.noreply.github.com>
Date:   Sun Jan 17 06:23:32 2021 +0530

    feat(area/kubernetes): show shared access policy in volume details (#4707)

commit 121d33538d
Author: Alice Groux <alice.grx@gmail.com>
Date:   Fri Jan 15 02:51:36 2021 +0100

    fix(k8s/application): validate load balancer ports inputs (#4426)

    * fix(k8s/application): validate load balancer ports inputs

    * fix(k8s/application): allow user to only change the protocol on the first port mapping

commit 7a03351df8
Author: Olli Janatuinen <olljanat@users.noreply.github.com>
Date:   Thu Jan 14 23:05:33 2021 +0200

    dep(api): Support Docker Stack 3.8 (#4333)

    - Linux: Update Docker binary to version 19.03.13
    - Windows: Update Docker binary to version 19.03.12

commit 0c2987893d
Author: Alice Groux <alice.grx@gmail.com>
Date:   Thu Jan 14 03:04:44 2021 +0100

    feat(app/images): in advanced mode, remove tooltip and add an information message (#4528)

commit d1eddaa188
Author: Alice Groux <alice.grx@gmail.com>
Date:   Thu Jan 14 00:24:56 2021 +0100

    feat(app/network): rename restrict external acces to the network label and add a tooltip (#4514)

commit d336ada3c2
Author: Anthony Lapenna <anthony.lapenna@portainer.io>
Date:   Wed Jan 13 16:13:27 2021 +1300

    feat(k8s/application): review application creation warning style (#4613)

commit 839198fbff
Author: Avadhut Tanugade <30384908+mrwhoknows55@users.noreply.github.com>
Date:   Wed Jan 13 04:49:18 2021 +0530

commit 486ffa5bbd
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Tue Jan 12 23:40:09 2021 +0200

    chore(webpack): add source maps (#4471)

    * chore(webpack): add source maps

    * feat(build): fetch source maps for 3rd party libs

commit 4cd468ce21
Author: Maxime Bajeux <max.bajeux@gmail.com>
Date:   Tue Jan 12 02:35:59 2021 +0100

    Can't create kubernetes resources with a username longer than 63 characters (#4672)

    * fix(kubernetes): truncate username when we create resource

    * fix(k8s): remove forbidden characters in owner label

commit cbd7fdc62e
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Tue Jan 12 01:38:49 2021 +0200

    feat(docker/stacks): introduce date info for stacks (#4660)

    * feat(docker/stacks): add creation and update dates

    * feat(docker/stacks): put ownership column as the last column

    * feat(docker/stacks): fix the no stacks message

    * refactor(docker/stacks): make external stacks helpers more readable

    * feat(docker/stacks): add updated and created by

    * feat(docker/stacks): toggle updated column

    * refactor(datatable): create column visibility component

    Co-authored-by: alice groux <alice.grx@gmail.com>

commit b9fe8009dd
Author: DarkAEther <30438425+DarkAEther@users.noreply.github.com>
Date:   Mon Jan 11 08:05:19 2021 +0530

    feat(image-details): Show labels in images datatable (#4287)

    * feat(images): show labels in images datatable

    * move labels to image details view

commit 6a504e7134
Author: Stéphane Busso <sbusso@users.noreply.github.com>
Date:   Mon Jan 11 14:44:15 2021 +1300

    fix(settings): Use default setting if UserSessionTimeout not set (#4521)

    * fix(settings): Use default settings if UserSessionTimeout not set

    * Update UserSessionTimeout settings in database if set to empty string

commit 51ba0876a5
Author: Alice Groux <alice.grx@gmail.com>
Date:   Mon Jan 11 00:51:46 2021 +0100

    feat(k8s/configuration): rename add ingress controller button and changed information text (#4540)

commit 769e6a4c6c
Author: Alice Groux <alice.grx@gmail.com>
Date:   Sun Jan 10 23:30:31 2021 +0100

    feat(k8s/configuration): add extra information panel when creating a sensitive configuration (#4541)

commit 105d1ae519
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 15:30:43 2021 +1300

    feat(frontend): de-emphasize internal login when OAuth is enabled (#3065) (#4565)

    * feat(frontend): de-emphasize internal login when OAuth is enabled (#3065)

    * feat(frontend): change the "Use internal authentication" style to be primary (#3065)

    * feat(frontend): resize the login with "provider" button to use a 120% font size (#3065)

    * feat(frontend): remove unused css for h1 tag (#3065)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit cf508065ec
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 12:51:27 2021 +1300

    fix(frontend): application edit page initializes the overridenKeyType of new added configuration key  to NONE so that the user can select how to load it (#4548) (#4593)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit eab828279e
Author: itsconquest <william.conquest@portainer.io>
Date:   Fri Jan 8 12:46:57 2021 +1300

    chore(project): exclude refactors (#4689)

commit d5763a970b
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 12:45:06 2021 +1300

    fix(frontend): Resource pool 'created' attribute is showing the time you view it at & not actual creation time (#4568) (#4599)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit c9f68a4d8f
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 11:55:42 2021 +1300

    fix(kubernetes): removes kube client cache when edge proxy is removed (#4487) (#4574)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 7848bcf2f4
Author: Alice Groux <alice.grx@gmail.com>
Date:   Thu Jan 7 22:29:17 2021 +0100

    feat(k8s/resources-list-view): add advanced deployment panel to resources list view (#4516)

    * feat(k8s/resources-list-view): add advanced deployment panel to applications view, configurations view and volumes view

    * feat(k8s/resources-list-view): move advanced deployment into a template and use it everywhere

commit b924347c5b
Author: Stéphane Busso <stephane.busso@gmail.com>
Date:   Thu Jan 7 14:03:46 2021 +1300

    Bump portainer version

commit 9fbda9fb99
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Thu Jan 7 13:38:01 2021 +1300

    Merge in release fixes to develop (#4687)

    * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)

    * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

    * fix(frontend) rephrase comments (#4629)

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    * + silently continue when downloading artifacts in windows (#4637)

    * fix(docker/stack-details): do not display editor tab for external stack (#4650)

    * Revert "chore(build): bump Kompose version (#4475)" (#4676)

    This reverts commit 380f106571.

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    Co-authored-by: cong meng <mcpacino@gmail.com>
    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
    Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

commit 82f8062784
Author: Anthony Lapenna <lapenna.anthony@gmail.com>
Date:   Wed Jan 6 11:31:05 2021 +1300

    chore(github): update issue template

commit 49982eb98a
Author: knittl <knittl89+github@gmail.com>
Date:   Tue Jan 5 20:49:50 2021 +0100

commit 4be3ac470f
Merge: 7975ef79 a50ab51b
Author: Stéphane Busso <sbusso@users.noreply.github.com>
Date:   Thu Dec 24 23:45:53 2020 +1300

    Merge pull request #4658 from portainer/revert-4475-chore-ce-86-bump-kompose-version

    Revert "chore(build): bump Kompose version"

commit a50ab51bef
Author: Stéphane Busso <sbusso@users.noreply.github.com>
Date:   Thu Dec 24 12:12:28 2020 +1300

    Revert "chore(build): bump Kompose version (#4475)"

    This reverts commit 380f106571.
2021-02-04 18:08:27 +13:00
yi-portainer
535215833d * version change to 2.1.1 2021-02-04 18:04:18 +13:00
yi-portainer
666b09ad3b Squashed commit of the following:
commit c4a1243af9
Author: Dmitry Salakhov <to@dimasalakhov.com>
Date:   Thu Feb 4 03:00:25 2021 +0000

    fix: docker-compose use custom config.json to access private images (#4820)

commit 305d0d2da0
Author: LP B <xAt0mZ@users.noreply.github.com>
Date:   Wed Feb 3 06:38:56 2021 +0100

    fix(k8s/resource-pool): unusable RP access management (#4810)

    (cherry picked from commit e401724d43)

commit e4605d990d
Author: yi-portainer <yi.chen@portainer.io>
Date:   Tue Feb 2 17:42:57 2021 +1300

    * update portainer version

commit 768697157c
Author: LP B <xAt0mZ@users.noreply.github.com>
Date:   Tue Feb 2 05:00:19 2021 +0100

    sec(app): remove unused and vulnerable dependencies (#4801)

commit d3086da139
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Feb 2 15:10:06 2021 +1300

    fix(k8s) trigger port validation while changing protocol (ce#394) (#4804)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 95894e8047
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Feb 2 15:03:11 2021 +1300

    fix(k8s) parse empty configuration as empty string yaml instead of {} (ce#395) (#4805)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 81de55fedd
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Tue Feb 2 11:12:40 2021 +1300

    * fix missing kubectl download (#4802)

commit 84827b8782
Author: Steven Kang <skan070@gmail.com>
Date:   Sun Jan 31 17:32:30 2021 +1300

    feat(build): introducing buildx for Windows (#4792)

    * feat(build): introducing buildx for Windows

    * feat(build): re-ordered USER

    * feat(build): Fixed Typo

    * feat(build): fixed typo

commit a71e71f481
Author: Dmitry Salakhov <to@dimasalakhov.com>
Date:   Mon Jan 25 19:16:53 2021 +0000

    feat(compose): add docker-compose wrapper (#4713)

    * feat(compose): add docker-compose wrapper

    ce-187

    * fix(compose): pick compose implementation upon startup

    * Add static compose build for linux

    * Fix wget

    * Fix platofrm specific docker-compose download

    * Keep amd64 architecture as download parameter

    * Add tmp folder for docker-compose

    * fix: line endings

    * add proxy server

    * logs

    * Proxy

    * Add lite transport for compose

    * Fix local deployment

    * refactor: pass proxyManager by ref

    * fix: string conversion

    * refactor: compose wrapper remove unused code

    * fix: tests

    * Add edge

    * Fix merge issue

    * refactor: remove unused code

    * Move server to proxy implementation

    * Cleanup wrapper and manager

    * feat: pass max supported compose syntax version with each endpoint

    * fix: pick compose syntax version

    * fix: store wrapper version in portainer

    * Get and show composeSyntaxMaxVersion at stack creation screen

    * Get and show composeSyntaxMaxVersion at stack editor screen

    * refactor: proxy server

    * Fix used tmp

    * Bump docker-compose to 1.28.0

    * remove message for docker compose limitation

    * fix: markup typo

    * Rollback docker compose to 1.27.4

    * * attempt to fix the windows build issue

    * * attempt to debug grunt issue

    * * use console log in grunt file

    * fix: try to fix windows build by removing indirect deps from go.mod

    * Remove tmp folder

    * Remove builder stage

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose - fixed verbose output

    * refactor: renames

    * fix(stack): get endpoint by EndpointProvider

    * fix(stack): use margin to add space between line instead of using br tag

    Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: yi-portainer <yi.chen@portainer.io>
    Co-authored-by: Steven Kang <skan070@gmail.com>

commit 83f4c5ec0b
Author: LP B <xAt0mZ@users.noreply.github.com>
Date:   Mon Jan 25 02:43:54 2021 +0100

    fix(k8s/app): remove advanced deployment panel from app details view (#4730)

commit 41308d570d
Author: Maxime Bajeux <max.bajeux@gmail.com>
Date:   Mon Jan 25 02:14:35 2021 +0100

    feat(configurations): Review UI/UX configurations (#4691)

    * feat(configurations): Review UI/UX configurations

    * feat(configurations): fix binary secret value

    * fix(frontend): populate data between simple and advanced modes (#4503)

    * fix(configuration): parseYaml before create configuration

    * fix(configurations): change c to C in ConfigurationOwner

    * fix(application): change configuration index to configuration key in the view

    * fix(configuration): resolve problem in application create with configuration not overriden.

    * fix(configuration): fix bad import in helper

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 46ff8a01bc
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Fri Jan 22 03:08:08 2021 +0200

    fix(kubernetes/pods): save note (#4675)

    * feat(kubernetes/pods): introduce patch api

    * feat(k8s/pods): pod converter

    * feat(kubernetes/pods): introduce patch api

    * feat(k8s/pod): add annotations only if needed

    * fix(k8s/pod): replace class with factory function

commit 2b257d2785
Author: yi-portainer <yi.chen@portainer.io>
Date:   Thu Jan 21 00:02:22 2021 +1300

    Squashed commit of the following 2.0.1 release fixes:

    commit f90d6b55d6
    Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
    Date:   Wed Jan 13 00:56:19 2021 +0200

        feat(service): clear source volume when change type (#4627)

        * feat(service): clear source volume when change type

        * feat(service): init volume source to the correct value

    commit 1b82b450d7
    Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
    Date:   Thu Jan 7 14:47:32 2021 +1300

        * bump the APIVersion to 2.0.1 (#4688)

    commit b78d804881
    Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
    Date:   Wed Dec 30 23:03:43 2020 +1300

        Revert "chore(build): bump Kompose version (#4475)" (#4676)

        This reverts commit 380f106571.

        Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    commit 51b72c12f9
    Author: Anthony Lapenna <anthony.lapenna@portainer.io>
    Date:   Wed Dec 23 14:45:32 2020 +1300

        fix(docker/stack-details): do not display editor tab for external stack (#4650)

    commit 58c04bdbe3
    Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
    Date:   Tue Dec 22 13:47:11 2020 +1300

        + silently continue when downloading artifacts in windows (#4637)

    commit a6320d5222
    Author: cong meng <mcpacino@gmail.com>
    Date:   Tue Dec 22 13:38:54 2020 +1300

        fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)

        * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

        * fix(frontend) rephrase comments (#4629)

        Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

        Co-authored-by: Simon Meng <simon.meng@portainer.io>
        Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

commit da41dbb79a
Author: cong meng <mcpacino@gmail.com>
Date:   Wed Jan 20 15:19:35 2021 +1300

    fix(stack): stacks created via API are incorrectly marked as private with no owner (#3721) (#4725)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 68d42617f2
Author: Maxime Bajeux <max.bajeux@gmail.com>
Date:   Wed Jan 20 01:02:18 2021 +0100

    feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster (#4525)

    * feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster

    * fix(applications): if there is at least one node the application can schedule on, then do not show the warning

commit 8323e22309
Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com>
Date:   Wed Jan 20 12:06:25 2021 +1300

    Update issue templates

    Adding auto labelling to Bug Report (kind/bug, bug/unconfirmed) and Question (kind/question)

commit 20d4341170
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Tue Jan 19 00:10:08 2021 +0200

    fix(state): check validity of state (#4609)

commit 832cafc933
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Mon Jan 18 02:59:57 2021 +0200

    fix(registries): update password only when not empty (#4669)

commit f3c537ac2c
Author: cong meng <mcpacino@gmail.com>
Date:   Mon Jan 18 13:02:16 2021 +1300

    chore(build): bump Kompose version (#4473) (#4724)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 958baf6283
Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com>
Date:   Mon Jan 18 09:30:17 2021 +1300

    Update README.md

commit 08e392378e
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Sun Jan 17 09:28:09 2021 +0200

    chore(app): fail on angular components missing nginject (#4224)

commit a2d9734b8b
Author: Alice Groux <alice.grx@gmail.com>
Date:   Sun Jan 17 04:50:22 2021 +0100

    fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable (#4511)

    * fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable

    * fix(k8s/datatables): reduce size of expand/collapse column

commit 15aed9fc6f
Author: DarkAEther <30438425+DarkAEther@users.noreply.github.com>
Date:   Sun Jan 17 06:23:32 2021 +0530

    feat(area/kubernetes): show shared access policy in volume details (#4707)

commit 121d33538d
Author: Alice Groux <alice.grx@gmail.com>
Date:   Fri Jan 15 02:51:36 2021 +0100

    fix(k8s/application): validate load balancer ports inputs (#4426)

    * fix(k8s/application): validate load balancer ports inputs

    * fix(k8s/application): allow user to only change the protocol on the first port mapping

commit 7a03351df8
Author: Olli Janatuinen <olljanat@users.noreply.github.com>
Date:   Thu Jan 14 23:05:33 2021 +0200

    dep(api): Support Docker Stack 3.8 (#4333)

    - Linux: Update Docker binary to version 19.03.13
    - Windows: Update Docker binary to version 19.03.12

commit 0c2987893d
Author: Alice Groux <alice.grx@gmail.com>
Date:   Thu Jan 14 03:04:44 2021 +0100

    feat(app/images): in advanced mode, remove tooltip and add an information message (#4528)

commit d1eddaa188
Author: Alice Groux <alice.grx@gmail.com>
Date:   Thu Jan 14 00:24:56 2021 +0100

    feat(app/network): rename restrict external acces to the network label and add a tooltip (#4514)

commit d336ada3c2
Author: Anthony Lapenna <anthony.lapenna@portainer.io>
Date:   Wed Jan 13 16:13:27 2021 +1300

    feat(k8s/application): review application creation warning style (#4613)

commit 839198fbff
Author: Avadhut Tanugade <30384908+mrwhoknows55@users.noreply.github.com>
Date:   Wed Jan 13 04:49:18 2021 +0530

commit 486ffa5bbd
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Tue Jan 12 23:40:09 2021 +0200

    chore(webpack): add source maps (#4471)

    * chore(webpack): add source maps

    * feat(build): fetch source maps for 3rd party libs

commit 4cd468ce21
Author: Maxime Bajeux <max.bajeux@gmail.com>
Date:   Tue Jan 12 02:35:59 2021 +0100

    Can't create kubernetes resources with a username longer than 63 characters (#4672)

    * fix(kubernetes): truncate username when we create resource

    * fix(k8s): remove forbidden characters in owner label

commit cbd7fdc62e
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Tue Jan 12 01:38:49 2021 +0200

    feat(docker/stacks): introduce date info for stacks (#4660)

    * feat(docker/stacks): add creation and update dates

    * feat(docker/stacks): put ownership column as the last column

    * feat(docker/stacks): fix the no stacks message

    * refactor(docker/stacks): make external stacks helpers more readable

    * feat(docker/stacks): add updated and created by

    * feat(docker/stacks): toggle updated column

    * refactor(datatable): create column visibility component

    Co-authored-by: alice groux <alice.grx@gmail.com>

commit b9fe8009dd
Author: DarkAEther <30438425+DarkAEther@users.noreply.github.com>
Date:   Mon Jan 11 08:05:19 2021 +0530

    feat(image-details): Show labels in images datatable (#4287)

    * feat(images): show labels in images datatable

    * move labels to image details view

commit 6a504e7134
Author: Stéphane Busso <sbusso@users.noreply.github.com>
Date:   Mon Jan 11 14:44:15 2021 +1300

    fix(settings): Use default setting if UserSessionTimeout not set (#4521)

    * fix(settings): Use default settings if UserSessionTimeout not set

    * Update UserSessionTimeout settings in database if set to empty string

commit 51ba0876a5
Author: Alice Groux <alice.grx@gmail.com>
Date:   Mon Jan 11 00:51:46 2021 +0100

    feat(k8s/configuration): rename add ingress controller button and changed information text (#4540)

commit 769e6a4c6c
Author: Alice Groux <alice.grx@gmail.com>
Date:   Sun Jan 10 23:30:31 2021 +0100

    feat(k8s/configuration): add extra information panel when creating a sensitive configuration (#4541)

commit 105d1ae519
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 15:30:43 2021 +1300

    feat(frontend): de-emphasize internal login when OAuth is enabled (#3065) (#4565)

    * feat(frontend): de-emphasize internal login when OAuth is enabled (#3065)

    * feat(frontend): change the "Use internal authentication" style to be primary (#3065)

    * feat(frontend): resize the login with "provider" button to use a 120% font size (#3065)

    * feat(frontend): remove unused css for h1 tag (#3065)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit cf508065ec
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 12:51:27 2021 +1300

    fix(frontend): application edit page initializes the overridenKeyType of new added configuration key  to NONE so that the user can select how to load it (#4548) (#4593)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit eab828279e
Author: itsconquest <william.conquest@portainer.io>
Date:   Fri Jan 8 12:46:57 2021 +1300

    chore(project): exclude refactors (#4689)

commit d5763a970b
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 12:45:06 2021 +1300

    fix(frontend): Resource pool 'created' attribute is showing the time you view it at & not actual creation time (#4568) (#4599)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit c9f68a4d8f
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 11:55:42 2021 +1300

    fix(kubernetes): removes kube client cache when edge proxy is removed (#4487) (#4574)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 7848bcf2f4
Author: Alice Groux <alice.grx@gmail.com>
Date:   Thu Jan 7 22:29:17 2021 +0100

    feat(k8s/resources-list-view): add advanced deployment panel to resources list view (#4516)

    * feat(k8s/resources-list-view): add advanced deployment panel to applications view, configurations view and volumes view

    * feat(k8s/resources-list-view): move advanced deployment into a template and use it everywhere

commit b924347c5b
Author: Stéphane Busso <stephane.busso@gmail.com>
Date:   Thu Jan 7 14:03:46 2021 +1300

    Bump portainer version

commit 9fbda9fb99
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Thu Jan 7 13:38:01 2021 +1300

    Merge in release fixes to develop (#4687)

    * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)

    * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

    * fix(frontend) rephrase comments (#4629)

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    * + silently continue when downloading artifacts in windows (#4637)

    * fix(docker/stack-details): do not display editor tab for external stack (#4650)

    * Revert "chore(build): bump Kompose version (#4475)" (#4676)

    This reverts commit 380f106571.

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    Co-authored-by: cong meng <mcpacino@gmail.com>
    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
    Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

commit 82f8062784
Author: Anthony Lapenna <lapenna.anthony@gmail.com>
Date:   Wed Jan 6 11:31:05 2021 +1300

    chore(github): update issue template

commit 49982eb98a
Author: knittl <knittl89+github@gmail.com>
Date:   Tue Jan 5 20:49:50 2021 +0100

commit 4be3ac470f
Merge: 7975ef79 a50ab51b
Author: Stéphane Busso <sbusso@users.noreply.github.com>
Date:   Thu Dec 24 23:45:53 2020 +1300

    Merge pull request #4658 from portainer/revert-4475-chore-ce-86-bump-kompose-version

    Revert "chore(build): bump Kompose version"

commit a50ab51bef
Author: Stéphane Busso <sbusso@users.noreply.github.com>
Date:   Thu Dec 24 12:12:28 2020 +1300

    Revert "chore(build): bump Kompose version (#4475)"

    This reverts commit 380f106571.
2021-02-04 17:28:23 +13:00
Dmitry Salakhov
c4a1243af9 fix: docker-compose use custom config.json to access private images (#4820) 2021-02-04 16:00:25 +13:00
LP B
305d0d2da0 fix(k8s/resource-pool): unusable RP access management (#4810)
(cherry picked from commit e401724d43)
2021-02-04 15:58:32 +13:00
yi-portainer
9af9b70f3e Squashed commit of the following:
commit e4605d990d
Author: yi-portainer <yi.chen@portainer.io>
Date:   Tue Feb 2 17:42:57 2021 +1300

    * update portainer version

commit 768697157c
Author: LP B <xAt0mZ@users.noreply.github.com>
Date:   Tue Feb 2 05:00:19 2021 +0100

    sec(app): remove unused and vulnerable dependencies (#4801)

commit d3086da139
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Feb 2 15:10:06 2021 +1300

    fix(k8s) trigger port validation while changing protocol (ce#394) (#4804)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 95894e8047
Author: cong meng <mcpacino@gmail.com>
Date:   Tue Feb 2 15:03:11 2021 +1300

    fix(k8s) parse empty configuration as empty string yaml instead of {} (ce#395) (#4805)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 81de55fedd
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Tue Feb 2 11:12:40 2021 +1300

    * fix missing kubectl download (#4802)

commit 84827b8782
Author: Steven Kang <skan070@gmail.com>
Date:   Sun Jan 31 17:32:30 2021 +1300

    feat(build): introducing buildx for Windows (#4792)

    * feat(build): introducing buildx for Windows

    * feat(build): re-ordered USER

    * feat(build): Fixed Typo

    * feat(build): fixed typo

commit a71e71f481
Author: Dmitry Salakhov <to@dimasalakhov.com>
Date:   Mon Jan 25 19:16:53 2021 +0000

    feat(compose): add docker-compose wrapper (#4713)

    * feat(compose): add docker-compose wrapper

    ce-187

    * fix(compose): pick compose implementation upon startup

    * Add static compose build for linux

    * Fix wget

    * Fix platofrm specific docker-compose download

    * Keep amd64 architecture as download parameter

    * Add tmp folder for docker-compose

    * fix: line endings

    * add proxy server

    * logs

    * Proxy

    * Add lite transport for compose

    * Fix local deployment

    * refactor: pass proxyManager by ref

    * fix: string conversion

    * refactor: compose wrapper remove unused code

    * fix: tests

    * Add edge

    * Fix merge issue

    * refactor: remove unused code

    * Move server to proxy implementation

    * Cleanup wrapper and manager

    * feat: pass max supported compose syntax version with each endpoint

    * fix: pick compose syntax version

    * fix: store wrapper version in portainer

    * Get and show composeSyntaxMaxVersion at stack creation screen

    * Get and show composeSyntaxMaxVersion at stack editor screen

    * refactor: proxy server

    * Fix used tmp

    * Bump docker-compose to 1.28.0

    * remove message for docker compose limitation

    * fix: markup typo

    * Rollback docker compose to 1.27.4

    * * attempt to fix the windows build issue

    * * attempt to debug grunt issue

    * * use console log in grunt file

    * fix: try to fix windows build by removing indirect deps from go.mod

    * Remove tmp folder

    * Remove builder stage

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose

    * feat(build/windows): add git for Docker Compose - fixed verbose output

    * refactor: renames

    * fix(stack): get endpoint by EndpointProvider

    * fix(stack): use margin to add space between line instead of using br tag

    Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: yi-portainer <yi.chen@portainer.io>
    Co-authored-by: Steven Kang <skan070@gmail.com>

commit 83f4c5ec0b
Author: LP B <xAt0mZ@users.noreply.github.com>
Date:   Mon Jan 25 02:43:54 2021 +0100

    fix(k8s/app): remove advanced deployment panel from app details view (#4730)

commit 41308d570d
Author: Maxime Bajeux <max.bajeux@gmail.com>
Date:   Mon Jan 25 02:14:35 2021 +0100

    feat(configurations): Review UI/UX configurations (#4691)

    * feat(configurations): Review UI/UX configurations

    * feat(configurations): fix binary secret value

    * fix(frontend): populate data between simple and advanced modes (#4503)

    * fix(configuration): parseYaml before create configuration

    * fix(configurations): change c to C in ConfigurationOwner

    * fix(application): change configuration index to configuration key in the view

    * fix(configuration): resolve problem in application create with configuration not overriden.

    * fix(configuration): fix bad import in helper

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 46ff8a01bc
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Fri Jan 22 03:08:08 2021 +0200

    fix(kubernetes/pods): save note (#4675)

    * feat(kubernetes/pods): introduce patch api

    * feat(k8s/pods): pod converter

    * feat(kubernetes/pods): introduce patch api

    * feat(k8s/pod): add annotations only if needed

    * fix(k8s/pod): replace class with factory function

commit 2b257d2785
Author: yi-portainer <yi.chen@portainer.io>
Date:   Thu Jan 21 00:02:22 2021 +1300

    Squashed commit of the following 2.0.1 release fixes:

    commit f90d6b55d6
    Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
    Date:   Wed Jan 13 00:56:19 2021 +0200

        feat(service): clear source volume when change type (#4627)

        * feat(service): clear source volume when change type

        * feat(service): init volume source to the correct value

    commit 1b82b450d7
    Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
    Date:   Thu Jan 7 14:47:32 2021 +1300

        * bump the APIVersion to 2.0.1 (#4688)

    commit b78d804881
    Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
    Date:   Wed Dec 30 23:03:43 2020 +1300

        Revert "chore(build): bump Kompose version (#4475)" (#4676)

        This reverts commit 380f106571.

        Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    commit 51b72c12f9
    Author: Anthony Lapenna <anthony.lapenna@portainer.io>
    Date:   Wed Dec 23 14:45:32 2020 +1300

        fix(docker/stack-details): do not display editor tab for external stack (#4650)

    commit 58c04bdbe3
    Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
    Date:   Tue Dec 22 13:47:11 2020 +1300

        + silently continue when downloading artifacts in windows (#4637)

    commit a6320d5222
    Author: cong meng <mcpacino@gmail.com>
    Date:   Tue Dec 22 13:38:54 2020 +1300

        fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)

        * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

        * fix(frontend) rephrase comments (#4629)

        Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

        Co-authored-by: Simon Meng <simon.meng@portainer.io>
        Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

commit da41dbb79a
Author: cong meng <mcpacino@gmail.com>
Date:   Wed Jan 20 15:19:35 2021 +1300

    fix(stack): stacks created via API are incorrectly marked as private with no owner (#3721) (#4725)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 68d42617f2
Author: Maxime Bajeux <max.bajeux@gmail.com>
Date:   Wed Jan 20 01:02:18 2021 +0100

    feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster (#4525)

    * feat(placement): Add a warning notification under the placement tab when an application cannot be scheduled on any node in the cluster

    * fix(applications): if there is at least one node the application can schedule on, then do not show the warning

commit 8323e22309
Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com>
Date:   Wed Jan 20 12:06:25 2021 +1300

    Update issue templates

    Adding auto labelling to Bug Report (kind/bug, bug/unconfirmed) and Question (kind/question)

commit 20d4341170
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Tue Jan 19 00:10:08 2021 +0200

    fix(state): check validity of state (#4609)

commit 832cafc933
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Mon Jan 18 02:59:57 2021 +0200

    fix(registries): update password only when not empty (#4669)

commit f3c537ac2c
Author: cong meng <mcpacino@gmail.com>
Date:   Mon Jan 18 13:02:16 2021 +1300

    chore(build): bump Kompose version (#4473) (#4724)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 958baf6283
Author: Anthony McMahon <75223906+Anthony-Portainer@users.noreply.github.com>
Date:   Mon Jan 18 09:30:17 2021 +1300

    Update README.md

commit 08e392378e
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Sun Jan 17 09:28:09 2021 +0200

    chore(app): fail on angular components missing nginject (#4224)

commit a2d9734b8b
Author: Alice Groux <alice.grx@gmail.com>
Date:   Sun Jan 17 04:50:22 2021 +0100

    fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable (#4511)

    * fix(k8s/datatables): reduce size of collapse/expand column for stacks datatable and storage datatable

    * fix(k8s/datatables): reduce size of expand/collapse column

commit 15aed9fc6f
Author: DarkAEther <30438425+DarkAEther@users.noreply.github.com>
Date:   Sun Jan 17 06:23:32 2021 +0530

    feat(area/kubernetes): show shared access policy in volume details (#4707)

commit 121d33538d
Author: Alice Groux <alice.grx@gmail.com>
Date:   Fri Jan 15 02:51:36 2021 +0100

    fix(k8s/application): validate load balancer ports inputs (#4426)

    * fix(k8s/application): validate load balancer ports inputs

    * fix(k8s/application): allow user to only change the protocol on the first port mapping

commit 7a03351df8
Author: Olli Janatuinen <olljanat@users.noreply.github.com>
Date:   Thu Jan 14 23:05:33 2021 +0200

    dep(api): Support Docker Stack 3.8 (#4333)

    - Linux: Update Docker binary to version 19.03.13
    - Windows: Update Docker binary to version 19.03.12

commit 0c2987893d
Author: Alice Groux <alice.grx@gmail.com>
Date:   Thu Jan 14 03:04:44 2021 +0100

    feat(app/images): in advanced mode, remove tooltip and add an information message (#4528)

commit d1eddaa188
Author: Alice Groux <alice.grx@gmail.com>
Date:   Thu Jan 14 00:24:56 2021 +0100

    feat(app/network): rename restrict external acces to the network label and add a tooltip (#4514)

commit d336ada3c2
Author: Anthony Lapenna <anthony.lapenna@portainer.io>
Date:   Wed Jan 13 16:13:27 2021 +1300

    feat(k8s/application): review application creation warning style (#4613)

commit 839198fbff
Author: Avadhut Tanugade <30384908+mrwhoknows55@users.noreply.github.com>
Date:   Wed Jan 13 04:49:18 2021 +0530

commit 486ffa5bbd
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Tue Jan 12 23:40:09 2021 +0200

    chore(webpack): add source maps (#4471)

    * chore(webpack): add source maps

    * feat(build): fetch source maps for 3rd party libs

commit 4cd468ce21
Author: Maxime Bajeux <max.bajeux@gmail.com>
Date:   Tue Jan 12 02:35:59 2021 +0100

    Can't create kubernetes resources with a username longer than 63 characters (#4672)

    * fix(kubernetes): truncate username when we create resource

    * fix(k8s): remove forbidden characters in owner label

commit cbd7fdc62e
Author: Chaim Lev-Ari <chiptus@users.noreply.github.com>
Date:   Tue Jan 12 01:38:49 2021 +0200

    feat(docker/stacks): introduce date info for stacks (#4660)

    * feat(docker/stacks): add creation and update dates

    * feat(docker/stacks): put ownership column as the last column

    * feat(docker/stacks): fix the no stacks message

    * refactor(docker/stacks): make external stacks helpers more readable

    * feat(docker/stacks): add updated and created by

    * feat(docker/stacks): toggle updated column

    * refactor(datatable): create column visibility component

    Co-authored-by: alice groux <alice.grx@gmail.com>

commit b9fe8009dd
Author: DarkAEther <30438425+DarkAEther@users.noreply.github.com>
Date:   Mon Jan 11 08:05:19 2021 +0530

    feat(image-details): Show labels in images datatable (#4287)

    * feat(images): show labels in images datatable

    * move labels to image details view

commit 6a504e7134
Author: Stéphane Busso <sbusso@users.noreply.github.com>
Date:   Mon Jan 11 14:44:15 2021 +1300

    fix(settings): Use default setting if UserSessionTimeout not set (#4521)

    * fix(settings): Use default settings if UserSessionTimeout not set

    * Update UserSessionTimeout settings in database if set to empty string

commit 51ba0876a5
Author: Alice Groux <alice.grx@gmail.com>
Date:   Mon Jan 11 00:51:46 2021 +0100

    feat(k8s/configuration): rename add ingress controller button and changed information text (#4540)

commit 769e6a4c6c
Author: Alice Groux <alice.grx@gmail.com>
Date:   Sun Jan 10 23:30:31 2021 +0100

    feat(k8s/configuration): add extra information panel when creating a sensitive configuration (#4541)

commit 105d1ae519
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 15:30:43 2021 +1300

    feat(frontend): de-emphasize internal login when OAuth is enabled (#3065) (#4565)

    * feat(frontend): de-emphasize internal login when OAuth is enabled (#3065)

    * feat(frontend): change the "Use internal authentication" style to be primary (#3065)

    * feat(frontend): resize the login with "provider" button to use a 120% font size (#3065)

    * feat(frontend): remove unused css for h1 tag (#3065)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit cf508065ec
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 12:51:27 2021 +1300

    fix(frontend): application edit page initializes the overridenKeyType of new added configuration key  to NONE so that the user can select how to load it (#4548) (#4593)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit eab828279e
Author: itsconquest <william.conquest@portainer.io>
Date:   Fri Jan 8 12:46:57 2021 +1300

    chore(project): exclude refactors (#4689)

commit d5763a970b
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 12:45:06 2021 +1300

    fix(frontend): Resource pool 'created' attribute is showing the time you view it at & not actual creation time (#4568) (#4599)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit c9f68a4d8f
Author: cong meng <mcpacino@gmail.com>
Date:   Fri Jan 8 11:55:42 2021 +1300

    fix(kubernetes): removes kube client cache when edge proxy is removed (#4487) (#4574)

    Co-authored-by: Simon Meng <simon.meng@portainer.io>

commit 7848bcf2f4
Author: Alice Groux <alice.grx@gmail.com>
Date:   Thu Jan 7 22:29:17 2021 +0100

    feat(k8s/resources-list-view): add advanced deployment panel to resources list view (#4516)

    * feat(k8s/resources-list-view): add advanced deployment panel to applications view, configurations view and volumes view

    * feat(k8s/resources-list-view): move advanced deployment into a template and use it everywhere

commit b924347c5b
Author: Stéphane Busso <stephane.busso@gmail.com>
Date:   Thu Jan 7 14:03:46 2021 +1300

    Bump portainer version

commit 9fbda9fb99
Author: Yi Chen <69284638+yi-portainer@users.noreply.github.com>
Date:   Thu Jan 7 13:38:01 2021 +1300

    Merge in release fixes to develop (#4687)

    * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)

    * fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

    * fix(frontend) rephrase comments (#4629)

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    * + silently continue when downloading artifacts in windows (#4637)

    * fix(docker/stack-details): do not display editor tab for external stack (#4650)

    * Revert "chore(build): bump Kompose version (#4475)" (#4676)

    This reverts commit 380f106571.

    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

    Co-authored-by: cong meng <mcpacino@gmail.com>
    Co-authored-by: Simon Meng <simon.meng@portainer.io>
    Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
    Co-authored-by: Anthony Lapenna <anthony.lapenna@portainer.io>

commit 82f8062784
Author: Anthony Lapenna <lapenna.anthony@gmail.com>
Date:   Wed Jan 6 11:31:05 2021 +1300

    chore(github): update issue template

commit 49982eb98a
Author: knittl <knittl89+github@gmail.com>
Date:   Tue Jan 5 20:49:50 2021 +0100

commit 4be3ac470f
Merge: 7975ef79 a50ab51b
Author: Stéphane Busso <sbusso@users.noreply.github.com>
Date:   Thu Dec 24 23:45:53 2020 +1300

    Merge pull request #4658 from portainer/revert-4475-chore-ce-86-bump-kompose-version

    Revert "chore(build): bump Kompose version"

commit a50ab51bef
Author: Stéphane Busso <sbusso@users.noreply.github.com>
Date:   Thu Dec 24 12:12:28 2020 +1300

    Revert "chore(build): bump Kompose version (#4475)"

    This reverts commit 380f106571.
2021-02-02 17:54:02 +13:00
yi-portainer
e4605d990d * update portainer version 2021-02-02 17:42:57 +13:00
LP B
768697157c sec(app): remove unused and vulnerable dependencies (#4801) 2021-02-02 17:02:06 +13:00
cong meng
d3086da139 fix(k8s) trigger port validation while changing protocol (ce#394) (#4804)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-02-02 15:10:06 +13:00
cong meng
95894e8047 fix(k8s) parse empty configuration as empty string yaml instead of {} (ce#395) (#4805)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-02-02 15:03:11 +13:00
Yi Chen
81de55fedd * fix missing kubectl download (#4802) 2021-02-02 11:12:40 +13:00
Steven Kang
84827b8782 feat(build): introducing buildx for Windows (#4792)
* feat(build): introducing buildx for Windows

* feat(build): re-ordered USER

* feat(build): Fixed Typo

* feat(build): fixed typo
2021-01-31 17:32:30 +13:00
yi-portainer
fa38af5d81 Merge remote-tracking branch 'origin/release/2.0.1' 2021-01-07 14:56:52 +13:00
Yi Chen
1b82b450d7 * bump the APIVersion to 2.0.1 (#4688) 2021-01-07 14:47:32 +13:00
Yi Chen
b78d804881 Revert "chore(build): bump Kompose version (#4475)" (#4676)
This reverts commit 380f106571.

Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2020-12-30 23:03:43 +13:00
Anthony Lapenna
51b72c12f9 fix(docker/stack-details): do not display editor tab for external stack (#4650) 2020-12-23 14:45:32 +13:00
Yi Chen
58c04bdbe3 + silently continue when downloading artifacts in windows (#4637) 2020-12-22 13:47:11 +13:00
cong meng
a6320d5222 fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180) (#4618)
* fix(frontend) unable to retrieve config map error when trying to manage newly created resource pool (ce#180)

* fix(frontend) rephrase comments (#4629)

Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>

Co-authored-by: Simon Meng <simon.meng@portainer.io>
Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2020-12-22 13:38:54 +13:00
Anthony Lapenna
cb4b4a43e6 update pull dog configuration 2020-08-31 18:09:19 +12:00
Anthony Lapenna
1e5a1d5bdd Merge branch 'develop' 2020-08-31 18:06:50 +12:00
Anthony Lapenna
5ed0d21c39 Merge branch 'ee-pulldog' 2020-08-28 15:26:30 +12:00
Anthony Lapenna
2972dbeafb feat(build/pulldog): review pulldog configuration 2020-08-18 12:36:01 +12:00
196 changed files with 5906 additions and 1278 deletions

View File

@@ -18,13 +18,16 @@ const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
var migrateLog = plog.NewScopedLog("bolt, migrate")
// FailSafeMigrate backup and restore DB if migration fail
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) error {
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) {
defer func() {
if err := recover(); err != nil {
migrateLog.Info(fmt.Sprintf("Error during migration, recovering [%v]", err))
if e := recover(); e != nil {
store.Rollback(true)
err = fmt.Errorf("%v", e)
}
}()
// !Important: we must use a named return value in the function definition and not a local
// !variable referenced from the closure or else the return value will be incorrectly set
return migrator.Migrate()
}

View File

@@ -301,7 +301,7 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 2.9.1
// Portainer 2.9.1, 2.9.2
if m.currentDBVersion < 33 {
err := m.migrateDBVersionToDB33()
if err != nil {
@@ -316,6 +316,13 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
if m.currentDBVersion < 35 {
if err := m.migrateDBVersionToDB35(); err != nil {
return migrationError(err, "migrateDBVersionToDB35")
}
}
err = m.versionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return migrationError(err, "StoreDBVersion")

View File

@@ -100,6 +100,32 @@ func (m *Migrator) updateDockerhubToDB32() error {
RegistryAccesses: portainer.RegistryAccesses{},
}
// The following code will make this function idempotent.
// i.e. if run again, it will not change the data. It will ensure that
// we only have one migrated registry entry. Duplicates will be removed
// if they exist and which has been happening due to earlier migration bugs
migrated := false
registries, _ := m.registryService.Registries()
for _, r := range registries {
if r.Type == registry.Type &&
r.Name == registry.Name &&
r.URL == registry.URL &&
r.Authentication == registry.Authentication {
if !migrated {
// keep this one entry
migrated = true
} else {
// delete subsequent duplicates
m.registryService.DeleteRegistry(portainer.RegistryID(r.ID))
}
}
}
if migrated {
return nil
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
@@ -218,8 +244,12 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
if !nameExist {
continue
}
createTime, createTimeExist := volume["CreatedAt"].(string)
if !createTimeExist {
continue
}
oldResourceID := fmt.Sprintf("%s%s", volumeName, volume["CreatedAt"].(string))
oldResourceID := fmt.Sprintf("%s%s", volumeName, createTime)
resourceControl, ok := volumeResourceControls[oldResourceID]
if ok {

View File

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

View File

@@ -0,0 +1,108 @@
package migrator
import (
"os"
"path"
"testing"
"time"
"github.com/boltdb/bolt"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/endpoint"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/stretchr/testify/assert"
)
const (
db35TestFile = "portainer-mig-35.db"
username = "portainer"
password = "password"
)
func setupDB35Test(t *testing.T) *Migrator {
is := assert.New(t)
dbConn, err := bolt.Open(path.Join(t.TempDir(), db35TestFile), 0600, &bolt.Options{Timeout: 1 * time.Second})
is.NoError(err, "failed to init testing DB connection")
// Create an old style dockerhub authenticated account
dockerhubService, err := dockerhub.NewService(&internal.DbConnection{DB: dbConn})
is.NoError(err, "failed to init testing registry service")
err = dockerhubService.UpdateDockerHub(&portainer.DockerHub{true, username, password})
is.NoError(err, "failed to create dockerhub account")
registryService, err := registry.NewService(&internal.DbConnection{DB: dbConn})
is.NoError(err, "failed to init testing registry service")
endpointService, err := endpoint.NewService(&internal.DbConnection{DB: dbConn})
is.NoError(err, "failed to init endpoint service")
m := &Migrator{
db: dbConn,
dockerhubService: dockerhubService,
registryService: registryService,
endpointService: endpointService,
}
return m
}
// TestUpdateDockerhubToDB32 tests a normal upgrade
func TestUpdateDockerhubToDB32(t *testing.T) {
is := assert.New(t)
m := setupDB35Test(t)
defer m.db.Close()
defer os.Remove(db35TestFile)
if err := m.updateDockerhubToDB32(); err != nil {
t.Errorf("failed to update settings: %v", err)
}
// Verify we have a single registry were created
registries, err := m.registryService.Registries()
is.NoError(err, "failed to read registries from the RegistryService")
is.Equal(len(registries), 1, "only one migrated registry expected")
}
// TestUpdateDockerhubToDB32_with_duplicate_migrations tests an upgrade where in earlier versions a broken migration
// created a large number of duplicate "dockerhub migrated" registry entries.
func TestUpdateDockerhubToDB32_with_duplicate_migrations(t *testing.T) {
is := assert.New(t)
m := setupDB35Test(t)
defer m.db.Close()
defer os.Remove(db35TestFile)
// Create lots of duplicate entries...
registry := &portainer.Registry{
Type: portainer.DockerHubRegistry,
Name: "Dockerhub (authenticated - migrated)",
URL: "docker.io",
Authentication: true,
Username: "portainer",
Password: "password",
RegistryAccesses: portainer.RegistryAccesses{},
}
for i := 1; i < 150; i++ {
err := m.registryService.CreateRegistry(registry)
assert.NoError(t, err, "create registry failed")
}
// Verify they were created
registries, err := m.registryService.Registries()
is.NoError(err, "failed to read registries from the RegistryService")
is.Condition(func() bool {
return len(registries) > 1
}, "expected multiple duplicate registry entries")
// Now run the migrator
if err := m.updateDockerhubToDB32(); err != nil {
t.Errorf("failed to update settings: %v", err)
}
// Verify we have a single registry were created
registries, err = m.registryService.Registries()
is.NoError(err, "failed to read registries from the RegistryService")
is.Equal(len(registries), 1, "only one migrated registry expected")
}

View File

@@ -192,8 +192,8 @@ func (service *Service) RefreshableStacks() ([]portainer.Stack, error) {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
var stack portainer.Stack
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
stack := portainer.Stack{}
err := internal.UnmarshalObject(v, &stack)
if err != nil {
return err

View File

@@ -93,7 +93,7 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
return "", nil, err
}
return fmt.Sprintf("http://127.0.0.1:%d", proxy.Port), proxy, nil
return fmt.Sprintf("tcp://127.0.0.1:%d", proxy.Port), proxy, nil
}
func createEnvFile(stack *portainer.Stack) (string, error) {

View File

@@ -18,6 +18,7 @@ import (
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/helm"
"github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
@@ -53,6 +54,7 @@ type Handler struct {
HelmTemplatesHandler *helm.Handler
KubernetesHandler *kubernetes.Handler
FileHandler *file.Handler
LDAPHandler *ldap.Handler
MOTDHandler *motd.Handler
RegistryHandler *registries.Handler
ResourceControlHandler *resourcecontrols.Handler
@@ -72,7 +74,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.9.1
// @version 2.9.3
// @description.markdown api-description.md
// @termsOfService
@@ -189,6 +191,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
default:
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
}
case strings.HasPrefix(r.URL.Path, "/api/ldap"):
http.StripPrefix("/api", h.LDAPHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/motd"):
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/registries"):

View File

@@ -0,0 +1,53 @@
package ldap
import (
"net/http"
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/security"
)
// Handler is the HTTP handler used to handle LDAP search Operations
type Handler struct {
*mux.Router
DataStore portainer.DataStore
FileService portainer.FileService
LDAPService portainer.LDAPService
}
// NewHandler returns a new Handler
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
}
h.Handle("/ldap/check",
bouncer.AdminAccess(httperror.LoggerHandler(h.ldapCheck))).Methods(http.MethodPost)
return h
}
func (handler *Handler) prefillSettings(ldapSettings *portainer.LDAPSettings) error {
if !ldapSettings.AnonymousMode && ldapSettings.Password == "" {
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return err
}
ldapSettings.Password = settings.LDAPSettings.Password
}
if (ldapSettings.TLSConfig.TLS || ldapSettings.StartTLS) && !ldapSettings.TLSConfig.TLSSkipVerify {
caCertPath, err := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
if err != nil {
return err
}
ldapSettings.TLSConfig.TLSCACertPath = caCertPath
}
return nil
}

View File

@@ -1,4 +1,4 @@
package settings
package ldap
import (
"net/http"
@@ -7,42 +7,43 @@ import (
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
)
type settingsLDAPCheckPayload struct {
type checkPayload struct {
LDAPSettings portainer.LDAPSettings
}
func (payload *settingsLDAPCheckPayload) Validate(r *http.Request) error {
func (payload *checkPayload) Validate(r *http.Request) error {
return nil
}
// @id SettingsLDAPCheck
// @id LDAPCheck
// @summary Test LDAP connectivity
// @description Test LDAP connectivity using LDAP details
// @description **Access policy**: administrator
// @tags settings
// @tags ldap
// @security jwt
// @accept json
// @param body body settingsLDAPCheckPayload true "details"
// @param body body checkPayload true "details"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /settings/ldap/check [put]
func (handler *Handler) settingsLDAPCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload settingsLDAPCheckPayload
// @router /ldap/check [post]
func (handler *Handler) ldapCheck(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload checkPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
if (payload.LDAPSettings.TLSConfig.TLS || payload.LDAPSettings.StartTLS) && !payload.LDAPSettings.TLSConfig.TLSSkipVerify {
caCertPath, _ := handler.FileService.GetPathForTLSFile(filesystem.LDAPStorePath, portainer.TLSFileCA)
payload.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
settings := &payload.LDAPSettings
err = handler.prefillSettings(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch default settings", err}
}
err = handler.LDAPService.TestConnectivity(&payload.LDAPSettings)
err = handler.LDAPService.TestConnectivity(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to connect to LDAP server", err}
}

View File

@@ -5,7 +5,7 @@ import (
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
)
@@ -35,8 +35,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut)
h.Handle("/settings/public",
bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet)
h.Handle("/settings/authentication/checkLDAP",
bouncer.AdminAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut)
return h
}

View File

@@ -54,11 +54,8 @@ func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
if payload.TemplatesURL != nil && *payload.TemplatesURL != "" && !govalidator.IsURL(*payload.TemplatesURL) {
return errors.New("Invalid external templates URL. Must correspond to a valid URL format")
}
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" {
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
if err != nil {
return errors.Wrap(err, "Invalid Helm repository URL. Must correspond to a valid URL format")
}
if payload.HelmRepositoryURL != nil && *payload.HelmRepositoryURL != "" && !govalidator.IsURL(*payload.HelmRepositoryURL) {
return errors.New("Invalid Helm repository URL. Must correspond to a valid URL format")
}
if payload.UserSessionTimeout != nil {
_, err := time.ParseDuration(*payload.UserSessionTimeout)
@@ -114,7 +111,16 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
}
if payload.HelmRepositoryURL != nil {
settings.HelmRepositoryURL = strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
newHelmRepo := strings.TrimSuffix(strings.ToLower(*payload.HelmRepositoryURL), "/")
if newHelmRepo != settings.HelmRepositoryURL && newHelmRepo != portainer.DefaultHelmRepositoryURL {
err := libhelm.ValidateHelmRepositoryURL(*payload.HelmRepositoryURL)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid Helm repository URL. Must correspond to a valid URL format", err}
}
}
settings.HelmRepositoryURL = newHelmRepo
}
if payload.BlackListedLabels != nil {

View File

@@ -10,13 +10,21 @@ import (
"github.com/portainer/portainer/api/http/security"
)
// websocketShellPodExec handles GET requests on /websocket/pod?token=<token>&endpointId=<endpointID>
// The request will be upgraded to the websocket protocol.
// Authentication and access is controlled via the mandatory token query parameter.
// The request will proxy input from the client to the pod via long-lived websocket connection.
// The following query parameters are mandatory:
// * token: JWT token used for authentication against this environment(endpoint)
// * endpointId: environment(endpoint) ID of the environment(endpoint) where the resource is located
// @summary Execute a websocket on kubectl shell pod
// @description The request will be upgraded to the websocket protocol. The request will proxy input from the client to the pod via long-lived websocket connection.
// @description **Access policy**: authenticated
// @security jwt
// @tags websocket
// @accept json
// @produce json
// @param endpointId query int true "environment(endpoint) ID of the environment(endpoint) where the resource is located"
// @param token query string true "JWT token used for authentication against this environment(endpoint)"
// @success 200
// @failure 400
// @failure 403
// @failure 404
// @failure 500
// @router /websocket/kubernetes-shell [get]
func (handler *Handler) websocketShellPodExec(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {

View File

@@ -46,5 +46,19 @@ func (transport *baseTransport) proxyNamespaceDeleteOperation(request *http.Requ
}
}
}
stacks, err := transport.dataStore.Stack().Stacks()
if err != nil {
return nil, err
}
for _, s := range stacks {
if s.Namespace == namespace && s.EndpointID == transport.endpoint.ID {
if err := transport.dataStore.Stack().DeleteStack(s.ID); err != nil {
return nil, err
}
}
}
return transport.executeKubernetesRequest(request)
}

View File

@@ -29,6 +29,7 @@ import (
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/helm"
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
"github.com/portainer/portainer/api/http/handler/motd"
"github.com/portainer/portainer/api/http/handler/registries"
"github.com/portainer/portainer/api/http/handler/resourcecontrols"
@@ -175,6 +176,11 @@ func (server *Server) Start() error {
var helmTemplatesHandler = helm.NewTemplateHandler(requestBouncer, server.HelmPackageManager)
var ldapHandler = ldap.NewHandler(requestBouncer)
ldapHandler.DataStore = server.DataStore
ldapHandler.FileService = server.FileService
ldapHandler.LDAPService = server.LDAPService
var motdHandler = motd.NewHandler(requestBouncer)
var registryHandler = registries.NewHandler(requestBouncer)
@@ -255,6 +261,7 @@ func (server *Server) Start() error {
EndpointEdgeHandler: endpointEdgeHandler,
EndpointProxyHandler: endpointProxyHandler,
FileHandler: fileHandler,
LDAPHandler: ldapHandler,
HelmTemplatesHandler: helmTemplatesHandler,
KubernetesHandler: kubernetesHandler,
MOTDHandler: motdHandler,

View File

@@ -9,7 +9,7 @@ import (
func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
return []rbacv1.PolicyRule{
{
Verbs: []string{"list"},
Verbs: []string{"list", "get"},
Resources: []string{"namespaces", "nodes"},
APIGroups: []string{""},
},
@@ -18,6 +18,11 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
Resources: []string{"storageclasses"},
APIGroups: []string{"storage.k8s.io"},
},
{
Verbs: []string{"list", "get"},
Resources: []string{"namespaces", "pods", "nodes"},
APIGroups: []string{"metrics.k8s.io"},
},
}
}

View File

@@ -1,11 +1,11 @@
package ldap
import (
"errors"
"fmt"
"strings"
ldap "github.com/go-ldap/ldap/v3"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
httperrors "github.com/portainer/portainer/api/http/errors"
@@ -20,55 +20,28 @@ var (
// Service represents a service used to authenticate users against a LDAP/AD.
type Service struct{}
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
var userDN string
found := false
usernameEscaped := ldap.EscapeFilter(username)
for _, searchSettings := range settings {
searchRequest := ldap.NewSearchRequest(
searchSettings.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, usernameEscaped),
[]string{"dn"},
nil,
)
// Deliberately skip errors on the search request so that we can jump to other search settings
// if any issue arise with the current one.
sr, err := conn.Search(searchRequest)
if err != nil {
continue
}
if len(sr.Entries) == 1 {
found = true
userDN = sr.Entries[0].DN
break
}
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
conn, err := createConnectionForURL(settings.URL, settings)
if err != nil {
return nil, errors.Wrap(err, "failed creating LDAP connection")
}
if !found {
return "", errUserNotFound
}
return userDN, nil
return conn, nil
}
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
func createConnectionForURL(url string, settings *portainer.LDAPSettings) (*ldap.Conn, error) {
if settings.TLSConfig.TLS || settings.StartTLS {
config, err := crypto.CreateTLSConfigurationFromDisk(settings.TLSConfig.TLSCACertPath, settings.TLSConfig.TLSCertPath, settings.TLSConfig.TLSKeyPath, settings.TLSConfig.TLSSkipVerify)
if err != nil {
return nil, err
}
config.ServerName = strings.Split(settings.URL, ":")[0]
config.ServerName = strings.Split(url, ":")[0]
if settings.TLSConfig.TLS {
return ldap.DialTLS("tcp", settings.URL, config)
return ldap.DialTLS("tcp", url, config)
}
conn, err := ldap.Dial("tcp", settings.URL)
conn, err := ldap.Dial("tcp", url)
if err != nil {
return nil, err
}
@@ -81,7 +54,7 @@ func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
return conn, nil
}
return ldap.Dial("tcp", settings.URL)
return ldap.Dial("tcp", url)
}
// AuthenticateUser is used to authenticate a user against a LDAP/AD.
@@ -133,13 +106,157 @@ func (*Service) GetUserGroups(username string, settings *portainer.LDAPSettings)
return nil, err
}
userGroups := getGroups(userDN, connection, settings.GroupSearchSettings)
userGroups := getGroupsByUser(userDN, connection, settings.GroupSearchSettings)
return userGroups, nil
}
// SearchUsers searches for users with the specified settings
func (*Service) SearchUsers(settings *portainer.LDAPSettings) ([]string, error) {
connection, err := createConnection(settings)
if err != nil {
return nil, err
}
defer connection.Close()
if !settings.AnonymousMode {
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return nil, err
}
}
users := map[string]bool{}
for _, searchSettings := range settings.SearchSettings {
searchRequest := ldap.NewSearchRequest(
searchSettings.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
searchSettings.Filter,
[]string{"dn", searchSettings.UserNameAttribute},
nil,
)
sr, err := connection.Search(searchRequest)
if err != nil {
return nil, err
}
for _, user := range sr.Entries {
username := user.GetAttributeValue(searchSettings.UserNameAttribute)
if username != "" {
users[username] = true
}
}
}
usersList := []string{}
for user := range users {
usersList = append(usersList, user)
}
return usersList, nil
}
// SearchGroups searches for groups with the specified settings
func (*Service) SearchGroups(settings *portainer.LDAPSettings) ([]portainer.LDAPUser, error) {
type groupSet map[string]bool
connection, err := createConnection(settings)
if err != nil {
return nil, err
}
defer connection.Close()
if !settings.AnonymousMode {
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return nil, err
}
}
userGroups := map[string]groupSet{}
for _, searchSettings := range settings.GroupSearchSettings {
searchRequest := ldap.NewSearchRequest(
searchSettings.GroupBaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
searchSettings.GroupFilter,
[]string{"cn", searchSettings.GroupAttribute},
nil,
)
sr, err := connection.Search(searchRequest)
if err != nil {
return nil, err
}
for _, entry := range sr.Entries {
members := entry.GetAttributeValues(searchSettings.GroupAttribute)
for _, username := range members {
_, ok := userGroups[username]
if !ok {
userGroups[username] = groupSet{}
}
userGroups[username][entry.GetAttributeValue("cn")] = true
}
}
}
users := []portainer.LDAPUser{}
for username, groups := range userGroups {
groupList := []string{}
for group := range groups {
groupList = append(groupList, group)
}
user := portainer.LDAPUser{
Name: username,
Groups: groupList,
}
users = append(users, user)
}
return users, nil
}
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
var userDN string
found := false
usernameEscaped := ldap.EscapeFilter(username)
for _, searchSettings := range settings {
searchRequest := ldap.NewSearchRequest(
searchSettings.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, usernameEscaped),
[]string{"dn"},
nil,
)
// Deliberately skip errors on the search request so that we can jump to other search settings
// if any issue arise with the current one.
sr, err := conn.Search(searchRequest)
if err != nil {
continue
}
if len(sr.Entries) == 1 {
found = true
userDN = sr.Entries[0].DN
break
}
}
if !found {
return "", errUserNotFound
}
return userDN, nil
}
// Get a list of group names for specified user from LDAP/AD
func getGroups(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
func getGroupsByUser(userDN string, conn *ldap.Conn, settings []portainer.LDAPGroupSearchSettings) []string {
groups := make([]string, 0)
userDNEscaped := ldap.EscapeFilter(userDN)
@@ -179,9 +296,18 @@ func (*Service) TestConnectivity(settings *portainer.LDAPSettings) error {
}
defer connection.Close()
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return err
if !settings.AnonymousMode {
err = connection.Bind(settings.ReaderDN, settings.Password)
if err != nil {
return err
}
} else {
err = connection.UnauthenticatedBind("")
if err != nil {
return err
}
}
return nil
}

View File

@@ -513,6 +513,12 @@ type (
AutoCreateUsers bool `json:"AutoCreateUsers" example:"true"`
}
// LDAPUser represents a LDAP user
LDAPUser struct {
Name string
Groups []string
}
// LicenseInformation represents information about an extension license
LicenseInformation struct {
LicenseKey string `json:"LicenseKey,omitempty"`
@@ -1295,6 +1301,8 @@ type (
AuthenticateUser(username, password string, settings *LDAPSettings) error
TestConnectivity(settings *LDAPSettings) error
GetUserGroups(username string, settings *LDAPSettings) ([]string, error)
SearchGroups(settings *LDAPSettings) ([]LDAPUser, error)
SearchUsers(settings *LDAPSettings) ([]string, error)
}
// OAuthService represents a service used to authenticate users using OAuth
@@ -1462,9 +1470,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.9.1"
APIVersion = "2.9.3"
// DBVersion is the version number of the Portainer database
DBVersion = 32
DBVersion = 35
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server

View File

@@ -721,10 +721,6 @@ a[ng-click] {
.multiSelect .multiSelectItem:hover,
.multiSelect .multiSelectGroup:hover {
border-color: var(--grey-3);
}
.multiSelect .multiSelectItem:hover,
.multiSelect .multiSelectGroup:hover {
background-image: var(--bg-image-multiselect) !important;
color: var(--white-color) !important;
}
@@ -816,10 +812,6 @@ json-tree .branch-preview {
}
/* !spinkit override */
.w-full {
width: 100%;
}
/* uib-typeahead override */
#scrollable-dropdown-menu .dropdown-menu {
max-height: 300px;
@@ -827,17 +819,33 @@ json-tree .branch-preview {
}
/* !uib-typeahead override */
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.kubectl-shell {
display: block;
text-align: center;
padding-bottom: 5px;
}
.w-full {
width: 100%;
}
.flex {
display: flex;
}
.block {
display: block;
}
.items-center {
align-items: center;
}
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.text-wrap {
word-break: break-all;
white-space: normal;

View File

@@ -89,8 +89,13 @@ html {
--green-1: #164;
--green-2: #1ec863;
--green-3: #23ae89;
--orange-1: #e86925;
--BE-only: var(--orange-1);
}
/* Default Theme */
:root {
--bg-card-color: var(--grey-10);
--bg-main-color: var(--white-color);

View File

@@ -246,12 +246,6 @@ json-tree .branch-preview {
.pagination > li > span:focus {
background-color: var(--bg-pagination-hover-color);
border-color: var(--border-pagination-hover-color);
}
.pagination > li > a:hover,
.pagination > li > span:hover,
.pagination > li > a:focus,
.pagination > li > span:focus {
color: var(--text-pagination-span-hover-color);
}
@@ -401,3 +395,14 @@ input:-webkit-autofill {
color: var(--white-color);
}
/* Overide Vendor CSS */
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn {
pointer-events: none;
touch-action: none;
}
.multiSelect.inlineBlock button {
margin: 0;
}

View File

@@ -69,13 +69,19 @@ class porImageRegistryController {
async reloadRegistries() {
return this.$async(async () => {
try {
const registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
this.registries = _.concat(this.defaultRegistry, registries);
let showDefaultRegistry = false;
this.registries = await this.EndpointService.registries(this.endpoint.Id, this.namespace);
// hide default(anonymous) dockerhub registry if user has an authenticated one
if (!this.registries.some((registry) => registry.Type === RegistryTypes.DOCKERHUB)) {
showDefaultRegistry = true;
this.registries.push(this.defaultRegistry);
}
const id = this.model.Registry.Id;
const registry = _.find(this.registries, { Id: id });
if (!registry) {
this.model.Registry = this.defaultRegistry;
this.model.Registry = showDefaultRegistry ? this.defaultRegistry : this.registries[0];
}
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve registries');

View File

@@ -6,7 +6,7 @@
</label>
<div ng-class="$ctrl.inputClass">
<select
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Name"
ng-options="registry as registry.Name for registry in $ctrl.registries track by registry.Id"
ng-model="$ctrl.model.Registry"
id="image_registry"
class="form-control"

View File

@@ -82,7 +82,7 @@
<!-- !tag-note -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!state.pullImageValidity || !formValues.RegistryModel.Image" ng-click="tagImage()">Tag</button>
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.RegistryModel.Image" ng-click="tagImage()">Tag</button>
</div>
</div>
</form>

View File

@@ -17,6 +17,8 @@ angular.module('portainer.docker').controller('ImageController', [
'FileSaver',
'Blob',
'endpoint',
'EndpointService',
'RegistryModalService',
function (
$async,
$q,
@@ -32,7 +34,9 @@ angular.module('portainer.docker').controller('ImageController', [
ModalService,
FileSaver,
Blob,
endpoint
endpoint,
EndpointService,
RegistryModalService
) {
$scope.endpoint = endpoint;
$scope.isAdmin = Authentication.isAdmin();
@@ -84,11 +88,13 @@ angular.module('portainer.docker').controller('ImageController', [
async function pushTag(repository) {
return $async(async () => {
$('#uploadResourceHint').show();
try {
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
await ImageService.pushImage(registryModel);
Notifications.success('Image successfully pushed', repository);
const registryModel = await RegistryModalService.registryModal(repository, $scope.registries);
if (registryModel) {
$('#uploadResourceHint').show();
await ImageService.pushImage(registryModel);
Notifications.success('Image successfully pushed', repository);
}
} catch (err) {
Notifications.error('Failure', err, 'Unable to push image to repository');
} finally {
@@ -100,11 +106,13 @@ angular.module('portainer.docker').controller('ImageController', [
$scope.pullTag = pullTag;
async function pullTag(repository) {
return $async(async () => {
$('#downloadResourceHint').show();
try {
const registryModel = await RegistryService.retrievePorRegistryModelFromRepository(repository, endpoint.Id);
await ImageService.pullImage(registryModel);
Notifications.success('Image successfully pulled', repository);
const registryModel = await RegistryModalService.registryModal(repository, $scope.registries);
if (registryModel) {
$('#downloadResourceHint').show();
await ImageService.pullImage(registryModel);
Notifications.success('Image successfully pulled', repository);
}
} catch (err) {
Notifications.error('Failure', err, 'Unable to pull image from repository');
} finally {
@@ -171,8 +179,15 @@ angular.module('portainer.docker').controller('ImageController', [
});
};
function initView() {
async function initView() {
HttpRequestHelper.setPortainerAgentTargetHeader($transition$.params().nodeName);
try {
$scope.registries = await RegistryService.loadRegistriesForDropdown(endpoint.Id);
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to load registries');
}
$q.all({
image: ImageService.image($transition$.params().id),
history: ImageService.history($transition$.params().id),

View File

@@ -1771,7 +1771,7 @@
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-primary btn-sm"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid()"
ng-click="ctrl.deployApplication()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppCreate-deployButton"

View File

@@ -2,6 +2,7 @@ import angular from 'angular';
import _ from 'lodash-es';
import filesizeParser from 'filesize-parser';
import * as JsonPatch from 'fast-json-patch';
import { RegistryTypes } from '@/portainer/models/registryTypes';
import {
KubernetesApplicationDataAccessPolicies,
@@ -193,6 +194,10 @@ class KubernetesCreateApplicationController {
this.state.pullImageValidity = validity;
}
imageValidityIsValid() {
return this.state.pullImageValidity || this.formValues.ImageModel.Registry.Type !== RegistryTypes.DOCKERHUB;
}
onChangeName() {
const existingApplication = _.find(this.applications, { Name: this.formValues.Name });
this.state.alreadyExists = (this.state.isEdit && existingApplication && this.application.Id !== existingApplication.Id) || (!this.state.isEdit && existingApplication);

View File

@@ -178,14 +178,14 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Allow resource over-commit
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" checked disabled /><i data-cy="kubeSetup-resourceOverCommitToggle"></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-setup-overcommit" target="_blank"> Portainer Business Edition</a>.
</span>
<por-switch-field
label="Allow resource over-commit"
name="resource-over-commit-switch"
feature="ctrl.limitedFeature"
ng-model="ctrl.formValues.EnableResourceOverCommit"
ng-change="ctrl.onChangeEnableResourceOverCommit()"
ng-data-cy="kubeSetup-resourceOverCommitToggle"
></por-switch-field>
</div>
</div>

View File

@@ -6,7 +6,7 @@ import { KubernetesIngressClass } from 'Kubernetes/ingress/models';
import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHelper';
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { K8S_SETUP_DEFAULT } from '@/portainer/feature-flags/feature-ids';
class KubernetesConfigureController {
/* #region CONSTRUCTOR */
@@ -38,6 +38,7 @@ class KubernetesConfigureController {
this.onInit = this.onInit.bind(this);
this.configureAsync = this.configureAsync.bind(this);
this.limitedFeature = K8S_SETUP_DEFAULT;
}
/* #endregion */

View File

@@ -63,7 +63,7 @@ class KubernetesDeployController {
RepositoryUsername: '',
RepositoryPassword: '',
AdditionalFiles: [],
ComposeFilePathInRepository: 'deployment.yml',
ComposeFilePathInRepository: '',
RepositoryAutomaticUpdates: true,
RepositoryMechanism: RepositoryMechanismTypes.INTERVAL,
RepositoryFetchInterval: '5m',

View File

@@ -162,14 +162,13 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Load Balancer quota
</label>
<label class="switch" style="margin-left: 20px;" data-cy="k8sNamespaceCreate-loadBalancerQuotaToggle"> <input type="checkbox" disabled /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
</span>
<por-switch-field
ng-data-cy="k8sNamespaceCreate-loadBalancerQuotaToggle"
label="Load Balancer quota"
name="k8s-resourcepool-Ibquota"
feature="$ctrl.LBQuotaFeatureId"
ng-model="lbquota"
></por-switch-field>
</div>
</div>
<!-- #endregion -->
@@ -192,15 +191,13 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Enable quota
</label>
<label class="switch" style="margin-left: 20px;" data-cy="k8sNamespaceCreate-enableQuotaToggle"> <input type="checkbox" disabled /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in
<a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-storagequota" target="_blank"> Portainer Business Edition</a>.
</span>
<por-switch-field
ng-data-cy="k8sNamespaceCreate-enableQuotaToggle"
label="Enable quota"
name="k8s-resourcepool-storagequota"
feature="$ctrl.StorageQuotaFeatureId"
ng-model="storagequota"
></por-switch-field>
</div>
</div>
<!-- #endregion -->

View File

@@ -12,6 +12,8 @@ import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHel
import { KubernetesFormValidationReferences } from 'Kubernetes/models/application/formValues';
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
import { K8S_RESOURCE_POOL_LB_QUOTA, K8S_RESOURCE_POOL_STORAGE_QUOTA } from '@/portainer/feature-flags/feature-ids';
class KubernetesCreateResourcePoolController {
/* #region CONSTRUCTOR */
/* @ngInject */
@@ -28,6 +30,8 @@ class KubernetesCreateResourcePoolController {
});
this.IngressClassTypes = KubernetesIngressClassTypes;
this.LBQuotaFeatureId = K8S_RESOURCE_POOL_LB_QUOTA;
this.StorageQuotaFeatureId = K8S_RESOURCE_POOL_STORAGE_QUOTA;
}
/* #endregion */

View File

@@ -146,14 +146,13 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Load Balancer quota
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-lbquota" target="_blank"> Portainer Business Edition</a>.
</span>
<por-switch-field
ng-data-cy="k8sNamespaceCreate-loadBalancerQuotaToggle"
label="Load Balancer quota"
name="k8s-resourcepool-Lbquota"
feature="ctrl.LBQuotaFeatureId"
ng-model="lbquota"
></por-switch-field>
</div>
</div>
<!-- #endregion -->
@@ -389,15 +388,13 @@
</div>
<div class="form-group">
<div class="col-sm-12">
<label class="control-label text-left">
Enable quota
</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" disabled /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in
<a href="https://www.portainer.io/business-upsell?from=k8s-resourcepool-storagequota" target="_blank"> Portainer Business Edition</a>.
</span>
<por-switch-field
ng-data-cy="k8sNamespaceCreate-enableQuotaToggle"
label="Enable quota"
name="k8s-resourcepool-storagequota"
feature="ctrl.StorageQuotaFeatureId"
ng-model="storagequota"
></por-switch-field>
</div>
</div>
<!-- #endregion -->

View File

@@ -16,6 +16,7 @@ import KubernetesFormValidationHelper from 'Kubernetes/helpers/formValidationHel
import { KubernetesIngressClassTypes } from 'Kubernetes/ingress/constants';
import KubernetesResourceQuotaConverter from 'Kubernetes/converters/resourceQuota';
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
import { K8S_RESOURCE_POOL_LB_QUOTA, K8S_RESOURCE_POOL_STORAGE_QUOTA } from '@/portainer/feature-flags/feature-ids';
class KubernetesResourcePoolController {
/* #region CONSTRUCTOR */
@@ -60,6 +61,9 @@ class KubernetesResourcePoolController {
this.IngressClassTypes = KubernetesIngressClassTypes;
this.ResourceQuotaDefaults = KubernetesResourceQuotaDefaults;
this.LBQuotaFeatureId = K8S_RESOURCE_POOL_LB_QUOTA;
this.StorageQuotaFeatureId = K8S_RESOURCE_POOL_STORAGE_QUOTA;
this.updateResourcePoolAsync = this.updateResourcePoolAsync.bind(this);
this.getEvents = this.getEvents.bind(this);
}

View File

@@ -1,7 +1,10 @@
import _ from 'lodash-es';
import './rbac';
import componentsModule from './components';
import settingsModule from './settings';
import featureFlagModule from './feature-flags';
import userActivityModule from './user-activity';
async function initAuthentication(authManager, Authentication, $rootScope, $state) {
authManager.checkAuthOnRefresh();
@@ -18,7 +21,7 @@ async function initAuthentication(authManager, Authentication, $rootScope, $stat
return await Authentication.init();
}
angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsModule]).config([
angular.module('portainer.app', ['portainer.oauth', 'portainer.rbac', componentsModule, settingsModule, featureFlagModule, userActivityModule, 'portainer.shared.datatable']).config([
'$stateRegistryProvider',
function ($stateRegistryProvider) {
'use strict';
@@ -51,6 +54,18 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsMo
controller: 'SidebarController',
},
},
resolve: {
featuresServiceInitialized: /* @ngInject */ function featuresServiceInitialized($async, featureService, Notifications) {
return $async(async () => {
try {
await featureService.init();
} catch (e) {
Notifications.error('Failed initializing features service', e);
throw e;
}
});
},
},
};
var endpointRoot = {
@@ -403,16 +418,6 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsMo
},
};
var roles = {
name: 'portainer.roles',
url: '/roles',
views: {
'content@': {
templateUrl: './views/roles/roles.html',
},
},
};
$stateRegistryProvider.register(root);
$stateRegistryProvider.register(endpointRoot);
$stateRegistryProvider.register(portainer);
@@ -444,7 +449,6 @@ angular.module('portainer.app', ['portainer.oauth', componentsModule, settingsMo
$stateRegistryProvider.register(user);
$stateRegistryProvider.register(teams);
$stateRegistryProvider.register(team);
$stateRegistryProvider.register(roles);
},
]);

View File

@@ -11,8 +11,8 @@
ng-if="$ctrl.options.length > 0"
input-model="$ctrl.options"
output-model="$ctrl.value"
button-label="icon '-' Name"
item-label="icon '-' Name"
button-label="icon Name"
item-label="icon Name"
tick-property="ticked"
helper-elements="filter"
search-property="Name"

View File

@@ -9,5 +9,6 @@ export const porAccessManagement = {
updateAccess: '<',
actionInProgress: '<',
filterUsers: '<',
limitedFeature: '<',
},
};

View File

@@ -4,17 +4,31 @@
<rd-widget-header icon="fa-user-lock" title-text="Create access"></rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div ng-if="ctrl.entityType !== 'registry'" class="form-group">
<span class="col-sm-12 small text-warning">
<p>
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Adding user access will require the affected user(s) to logout and login for the changes to be taken into account.
</p>
</span>
</div>
<por-access-management-users-selector options="ctrl.availableUsersAndTeams" value="ctrl.formValues.multiselectOutput"></por-access-management-users-selector>
<div class="form-group" ng-if="ctrl.entityType != 'registry'">
<div class="form-group" ng-if="ctrl.entityType !== 'registry'">
<label class="col-sm-3 col-lg-2 control-label text-left">
Role
</label>
<div class="col-sm-9 col-lg-4">
<span class="text-muted small">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in <a href="https://www.portainer.io/business-upsell?from=k8s-rbac-access" target="_blank"> Portainer Business Edition</a>.
</span>
<div class="col-sm-9 col-lg-6">
<div class="flex items-center">
<select
class="form-control"
ng-model="ctrl.formValues.selectedRole"
ng-options="role as ctrl.roleLabel(role) disable when ctrl.isRoleLimitedToBE(role) for role in ctrl.roles"
>
</select>
<be-feature-indicator feature="ctrl.limitedFeature" class="space-left"></be-feature-indicator>
</div>
</div>
</div>
@@ -48,6 +62,10 @@
title-icon="fa-user-lock"
table-key="{{ 'access_' + ctrl.entityType }}"
order-by="Name"
show-warning="ctrl.entityType !== 'registry'"
is-update-enabled="ctrl.entityType !== 'registry'"
show-roles="ctrl.entityType !== 'registry'"
roles="ctrl.roles"
inherit-from="ctrl.inheritFrom"
dataset="ctrl.authorizedUsersAndTeams"
update-action="ctrl.updateAction"

View File

@@ -1,12 +1,14 @@
import _ from 'lodash-es';
import angular from 'angular';
import { RoleTypes } from '@/portainer/rbac/models/role';
class PorAccessManagementController {
/* @ngInject */
constructor(Notifications, AccessService) {
this.Notifications = Notifications;
this.AccessService = AccessService;
constructor(Notifications, AccessService, RoleService, featureService) {
Object.assign(this, { Notifications, AccessService, RoleService, featureService });
this.limitedToBE = false;
this.unauthorizeAccess = this.unauthorizeAccess.bind(this);
this.updateAction = this.updateAction.bind(this);
@@ -29,10 +31,11 @@ class PorAccessManagementController {
const entity = this.accessControlledEntity;
const oldUserAccessPolicies = entity.UserAccessPolicies;
const oldTeamAccessPolicies = entity.TeamAccessPolicies;
const selectedRoleId = this.formValues.selectedRole.Id;
const selectedUserAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'user');
const selectedTeamAccesses = _.filter(this.formValues.multiselectOutput, (access) => access.Type === 'team');
const accessPolicies = this.AccessService.generateAccessPolicies(oldUserAccessPolicies, oldTeamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, 0);
const accessPolicies = this.AccessService.generateAccessPolicies(oldUserAccessPolicies, oldTeamAccessPolicies, selectedUserAccesses, selectedTeamAccesses, selectedRoleId);
this.accessControlledEntity.UserAccessPolicies = accessPolicies.userAccessPolicies;
this.accessControlledEntity.TeamAccessPolicies = accessPolicies.teamAccessPolicies;
this.updateAccess();
@@ -50,11 +53,41 @@ class PorAccessManagementController {
this.updateAccess();
}
isRoleLimitedToBE(role) {
if (!this.limitedToBE) {
return false;
}
return role.ID !== RoleTypes.STANDARD;
}
roleLabel(role) {
if (!this.limitedToBE) {
return role.Name;
}
if (this.isRoleLimitedToBE(role)) {
return `${role.Name} (Business Edition Feature)`;
}
return `${role.Name} (Default)`;
}
async $onInit() {
try {
if (this.limitedFeature) {
this.limitedToBE = this.featureService.isLimitedToBE(this.limitedFeature);
}
const entity = this.accessControlledEntity;
const parent = this.inheritFrom;
const roles = await this.RoleService.roles();
this.roles = _.orderBy(roles, 'Priority', 'asc');
this.formValues = {
selectedRole: this.roles.find((role) => !this.isRoleLimitedToBE(role)),
};
const data = await this.AccessService.accesses(entity, parent, this.roles);
if (this.filterUsers) {

View File

@@ -0,0 +1,18 @@
const BE_URL = 'https://www.portainer.io/business-upsell?from=';
export default class BeIndicatorController {
/* @ngInject */
constructor(featureService) {
Object.assign(this, { featureService });
this.limitedToBE = false;
}
$onInit() {
if (this.feature) {
this.url = `${BE_URL}${this.feature}`;
this.limitedToBE = this.featureService.isLimitedToBE(this.feature);
}
}
}

View File

@@ -0,0 +1,26 @@
.be-indicator {
border: solid 1px var(--BE-only);
border-radius: 15px;
padding: 5px 10px;
font-weight: 400;
touch-action: all;
pointer-events: all;
white-space: nowrap;
}
.be-indicator .be-indicator-icon {
color: #000000;
}
.be-indicator:hover {
text-decoration: none;
}
.be-indicator:hover .be-indicator-label {
text-decoration: underline;
}
.be-indicator-container {
border: solid 1px var(--BE-only);
margin: 15px;
}

View File

@@ -0,0 +1,5 @@
<a class="be-indicator" href="{{ $ctrl.url }}" target="_blank" rel="noopener" ng-if="$ctrl.limitedToBE">
<ng-transclude></ng-transclude>
<i class="be-indicator-icon fas fa-briefcase space-right"></i>
<span class="be-indicator-label">Business Edition Feature</span>
</a>

View File

@@ -0,0 +1,15 @@
import angular from 'angular';
import controller from './be-feature-indicator.controller.js';
import './be-feature-indicator.css';
export const beFeatureIndicator = {
templateUrl: './be-feature-indicator.html',
controller,
bindings: {
feature: '<',
},
transclude: true,
};
angular.module('portainer.app').component('beFeatureIndicator', beFeatureIndicator);

View File

@@ -0,0 +1,23 @@
export default class BoxSelectorItemController {
/* @ngInject */
constructor(featureService) {
Object.assign(this, { featureService });
this.limitedToBE = false;
}
handleChange(value) {
this.formCtrl.$setValidity(this.radioName, !this.limitedToBE, this.formCtrl);
this.onChange(value);
}
$onInit() {
if (this.option.feature) {
this.limitedToBE = this.featureService.isLimitedToBE(this.option.feature);
}
}
$onDestroy() {
this.formCtrl.$setValidity(this.radioName, true, this.formCtrl);
}
}

View File

@@ -0,0 +1,117 @@
.boxselector_wrapper > div,
.boxselector_wrapper box-selector-item {
--selected-item-color: var(--blue-2);
flex: 1;
padding: 0.5rem;
}
.boxselector_wrapper .boxselector_header {
font-size: 14px;
margin-bottom: 5px;
font-weight: bold;
user-select: none;
}
.boxselector_header .fa,
.fab {
font-weight: normal;
}
.boxselector_wrapper input[type='radio'] {
display: none;
}
.boxselector_wrapper input[type='radio']:not(:disabled) ~ label {
cursor: pointer;
background-color: var(--bg-boxselector-wrapper-disabled-color);
}
.boxselector_wrapper input[type='radio']:not(:disabled):hover ~ label:hover {
cursor: pointer;
}
.boxselector_wrapper label {
font-weight: normal;
font-size: 12px;
display: block;
background: var(--bg-boxselector-color);
border: 1px solid var(--border-boxselector-color);
border-radius: 2px;
padding: 10px 10px 0 10px;
text-align: center;
box-shadow: var(--shadow-boxselector-color);
position: relative;
}
.box-selector-item input:disabled + label,
.boxselector_wrapper label.boxselector_disabled {
background: var(--bg-boxselector-disabled-color) !important;
border-color: #787878;
color: #787878;
cursor: not-allowed;
pointer-events: none;
}
.boxselector_wrapper input[type='radio']:checked + label {
background: var(--selected-item-color);
color: white;
padding-top: 2rem;
border-color: var(--selected-item-color);
}
.boxselector_wrapper input[type='radio']:checked + label::after {
color: var(--selected-item-color);
font-family: 'Font Awesome 5 Free';
border: 2px solid var(--selected-item-color);
content: '\f00c';
font-size: 16px;
font-weight: bold;
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
height: 30px;
width: 30px;
line-height: 26px;
text-align: center;
border-radius: 50%;
background: white;
box-shadow: 0 2px 5px -2px rgba(0, 0, 0, 0.25);
}
@media only screen and (max-width: 700px) {
.boxselector_wrapper {
flex-direction: column;
}
}
.box-selector-item-description {
height: 1em;
}
.box-selector-item.limited.business {
--selected-item-color: var(--BE-only);
}
.box-selector-item.limited.business label {
border-color: var(--BE-only);
border-width: 2px;
}
.box-selector-item .limited-icon {
position: absolute;
left: 1em;
top: calc(50% - 0.5em);
height: 1em;
}
@media (min-width: 992px) {
.box-selector-item .limited-icon {
left: 2em;
}
}
.box-selector-item.limited.business :checked + label {
background-color: initial;
color: initial;
}

View File

@@ -1,5 +1,6 @@
<div
class="box-selector-item"
ng-class="{ business: $ctrl.limitedToBE, limited: $ctrl.limitedToBE }"
tooltip-append-to-body="true"
tooltip-placement="bottom"
tooltip-class="portainer-tooltip"
@@ -14,11 +15,13 @@
ng-value="$ctrl.option.value"
ng-disabled="$ctrl.disabled"
/>
<label for="{{ $ctrl.option.id }}" ng-click="$ctrl.onChange($ctrl.option.value)" t data-cy="edgeStackCreate-deploymentSelector_{{ $ctrl.option.value }}">
<label for="{{ $ctrl.option.id }}" ng-click="$ctrl.handleChange($ctrl.option.value)">
<i class="fas fa-briefcase limited-icon" ng-if="$ctrl.limitedToBE"></i>
<div class="boxselector_header">
<i ng-class="$ctrl.option.icon" aria-hidden="true" style="margin-right: 2px;"></i>
{{ $ctrl.option.label }}
</div>
<p ng-if="$ctrl.option.description">{{ $ctrl.option.description }}</p>
<p class="box-selector-item-description">{{ $ctrl.option.description }}</p>
</label>
</div>

View File

@@ -1,7 +1,15 @@
import angular from 'angular';
import './box-selector-item.css';
import controller from './box-selector-item.controller';
angular.module('portainer.app').component('boxSelectorItem', {
templateUrl: './box-selector-item.html',
controller,
require: {
formCtrl: '^^form',
},
bindings: {
radioName: '@',
isChecked: '<',

View File

@@ -4,10 +4,10 @@ export default class BoxSelectorController {
this.change = this.change.bind(this);
}
change(value) {
change(value, limited) {
this.ngModel = value;
if (this.onChange) {
this.onChange(value);
this.onChange(value, limited);
}
}

View File

@@ -3,89 +3,3 @@
flex-flow: row wrap;
margin: 0.5rem;
}
.boxselector_wrapper > div,
.boxselector_wrapper box-selector-item {
flex: 1;
padding: 0.5rem;
}
.boxselector_wrapper .boxselector_header {
font-size: 14px;
margin-bottom: 5px;
font-weight: bold;
user-select: none;
}
.boxselector_header .fa,
.fab {
font-weight: normal;
}
.boxselector_wrapper input[type='radio'] {
display: none;
}
.boxselector_wrapper input[type='radio']:not(:disabled) ~ label {
cursor: pointer;
background-color: var(--bg-boxselector-wrapper-disabled-color);
}
.boxselector_wrapper input[type='radio']:not(:disabled):hover ~ label:hover {
cursor: pointer;
}
.boxselector_wrapper label {
font-weight: normal;
font-size: 12px;
display: block;
background: var(--bg-boxselector-color);
border: 1px solid var(--border-boxselector-color);
border-radius: 2px;
padding: 10px 10px 0 10px;
text-align: center;
box-shadow: var(--shadow-boxselector-color);
position: relative;
}
.box-selector-item input:disabled + label,
.boxselector_wrapper label.boxselector_disabled {
background: var(--bg-boxselector-disabled-color) !important;
border-color: #787878;
color: #787878;
cursor: not-allowed;
pointer-events: none;
}
.boxselector_wrapper input[type='radio']:checked + label {
background: #337ab7;
color: white;
padding-top: 2rem;
border-color: #337ab7;
}
.boxselector_wrapper input[type='radio']:checked + label::after {
color: #337ab7;
font-family: 'Font Awesome 5 Free';
border: 2px solid #337ab7;
content: '\f00c';
font-size: 16px;
font-weight: bold;
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
height: 30px;
width: 30px;
line-height: 26px;
text-align: center;
border-radius: 50%;
background: white;
box-shadow: 0 2px 5px -2px rgba(0, 0, 0, 0.25);
}
@media only screen and (max-width: 700px) {
.boxselector_wrapper {
flex-direction: column;
}
}

View File

@@ -15,6 +15,6 @@ angular.module('portainer.app').component('boxSelector', {
},
});
export function buildOption(id, icon, label, description, value) {
return { id, icon, label, description, value };
export function buildOption(id, icon, label, description, value, feature) {
return { id, icon, label, description, value, feature };
}

View File

@@ -1,26 +0,0 @@
<div class="datatable">
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" placeholder="Search..." ng-model-options="{ debounce: 300 }" />
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
Environment
</th>
<th>
Role
</th>
<th>Access origin</th>
</tr>
</thead>
<tbody>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Select a user to show associated access and role</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -92,6 +92,10 @@
float: none;
}
.datatable.datatable-empty .table > tbody > tr > td {
padding: 15px 0;
}
.tableMenu {
color: #767676;
padding: 10px;

View File

@@ -0,0 +1,19 @@
export default class DatatableFilterController {
isEnabled() {
return 0 < this.state.length && this.state.length < this.labels.length;
}
onChangeItem(filterValue) {
if (this.isChecked(filterValue)) {
return this.onChange(
this.filterKey,
this.state.filter((v) => v !== filterValue)
);
}
return this.onChange(this.filterKey, [...this.state, filterValue]);
}
isChecked(filterValue) {
return this.state.includes(filterValue);
}
}

View File

@@ -0,0 +1,32 @@
<div uib-dropdown dropdown-append-to-body auto-close="outsideClick" is-open="$ctrl.isOpen">
<span ng-transclude></span>
<div class="filter-button">
<span uib-dropdown-toggle class="table-filter" ng-class="{ 'filter-active': $ctrl.isEnabled() }">
Filter
<i class="fa" ng-class="[$ctrl.isEnabled() ? 'fa-check' : 'fa-filter']" aria-hidden="true"></i>
</span>
</div>
<div class="dropdown-menu" style="min-width: 0;" uib-dropdown-menu>
<div class="tableMenu">
<div class="menuContent">
<div class="md-checkbox" ng-repeat="filter in $ctrl.labels track by filter.value">
<input
id="filter_{{ $ctrl.filterKey }}_{{ $index }}"
type="checkbox"
ng-value="filter.value"
ng-checked="$ctrl.state.includes(filter.value)"
ng-click="$ctrl.onChangeItem(filter.value)"
/>
<label for="filter_{{ $ctrl.filterKey }}_{{ $index }}">
{{ filter.label }}
</label>
</div>
</div>
<div>
<a class="btn btn-default btn-sm" ng-click="$ctrl.isOpen = false;">
Close
</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,13 @@
import controller from './datatable-filter.controller';
export const datatableFilter = {
bindings: {
labels: '<', // [{label, value}]
state: '<', // [filterValue]
filterKey: '@',
onChange: '<',
},
controller,
templateUrl: './datatable-filter.html',
transclude: true,
};

View File

@@ -128,7 +128,11 @@ angular.module('portainer.app').controller('GenericDatatableController', [
* https://github.com/portainer/portainer/pull/2877#issuecomment-503333425
* https://github.com/portainer/portainer/pull/2877#issuecomment-503537249
*/
this.$onInit = function () {
this.$onInit = function $onInit() {
this.$onInitGeneric();
};
this.$onInitGeneric = function $onInitGeneric() {
this.setDefaults();
this.prepareTableFromDataset();

View File

@@ -0,0 +1,16 @@
import angular from 'angular';
import 'angular-utils-pagination';
import { datatableTitlebar } from './titlebar';
import { datatableSearchbar } from './searchbar';
import { datatableSortIcon } from './sort-icon';
import { datatablePagination } from './pagination';
import { datatableFilter } from './filter';
export default angular
.module('portainer.shared.datatable', ['angularUtils.directives.dirPagination'])
.component('datatableTitlebar', datatableTitlebar)
.component('datatableSearchbar', datatableSearchbar)
.component('datatableSortIcon', datatableSortIcon)
.component('datatablePagination', datatablePagination)
.component('datatableFilter', datatableFilter).name;

View File

@@ -0,0 +1,9 @@
export const datatablePagination = {
bindings: {
onChangeLimit: '<',
limit: '<',
enableNoLimit: '<',
onChangePage: '<',
},
templateUrl: './pagination.html',
};

View File

@@ -0,0 +1,15 @@
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;"> Items per page </span>
<select class="form-control" ng-model="$ctrl.limit" ng-change="$ctrl.onChangeLimit($ctrl.limit)">
<option ng-if="$ctrl.enableNoLimit" ng-value="0">All</option>
<option ng-value="10">10</option>
<option ng-value="25">25</option>
<option ng-value="50">50</option>
<option ng-value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5" on-page-change="$ctrl.onChangePage(newPageNumber)"> </dir-pagination-controls>
</form>
</div>

View File

@@ -87,15 +87,10 @@
</td>
<td>
<a ng-if="$ctrl.canManageAccess(item)" ng-click="$ctrl.redirectToManageAccess(item)"> <i class="fa fa-users" aria-hidden="true"></i> Manage access </a>
<span
ng-if="$ctrl.canBrowse(item)"
class="text-muted space-left"
style="cursor: pointer;"
data-toggle="tooltip"
title="This feature is available in Portainer Business Edition"
>
<i class="fa fa-search" aria-hidden="true"></i> Browse</span
>
<be-feature-indicator feature="$ctrl.limitedFeature" ng-if="$ctrl.canBrowse(item)">
<span class="text-muted space-left" style="padding-right: 5px;"> <i class="fa fa-search" aria-hidden="true"></i> Browse</span>
</be-feature-indicator>
<span ng-if="!$ctrl.canBrowse(item) && !$ctrl.canManageAccess(item)"> - </span>
</td>
</tr>

View File

@@ -1,5 +1,5 @@
import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models';
import { REGISTRY_MANAGEMENT } from '@/portainer/feature-flags/feature-ids';
angular.module('portainer.docker').controller('RegistriesDatatableController', RegistriesDatatableController);
/* @ngInject */
@@ -45,6 +45,7 @@ function RegistriesDatatableController($scope, $controller, $state, Authenticati
};
this.$onInit = function () {
this.limitedFeature = REGISTRY_MANAGEMENT;
this.isAdmin = Authentication.isAdmin();
this.setDefaults();
this.prepareTableFromDataset();

View File

@@ -1,64 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" placeholder="Search..." auto-focus ng-model-options="{ debounce: 300 }" />
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
Name
</th>
<th>
Description
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="text-muted">Environment administrator</td>
<td class="text-muted">Full control of all resources in an environment</td>
</tr>
<tr>
<td class="text-muted">Helpdesk</td>
<td class="text-muted">Read-only access of all resources in an environment</td>
</tr>
<tr>
<td class="text-muted">Read-only user</td>
<td class="text-muted">Read-only access of assigned resources in an environment</td>
</tr>
<tr>
<td class="text-muted">Standard user</td>
<td class="text-muted">Full control of assigned resources in an environment</td>
</tr>
</tbody>
</table>
<div class="footer">
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,4 @@
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.filter" ng-change="$ctrl.onChange($ctrl.filter)" placeholder="Search..." ng-model-options="{ debounce: 300 }" />
</div>

View File

@@ -0,0 +1,7 @@
export const datatableSearchbar = {
bindings: {
onChange: '<',
ngModel: '<',
},
templateUrl: './datatable-searchbar.html',
};

View File

@@ -0,0 +1,5 @@
export default class datatableSortIconController {
isCurrentSortOrder() {
return this.selectedSortKey === this.key;
}
}

View File

@@ -0,0 +1,9 @@
<i
class="fa fa-sort-alpha-down"
ng-class="{
'fa-sort-alpha-down': !$ctrl.reverseOrder,
'fa-sort-alpha-up': $ctrl.reverseOrder,
}"
aria-hidden="true"
ng-if="$ctrl.isCurrentSortOrder()"
></i>

View File

@@ -0,0 +1,11 @@
import controller from './datatable-sort-icon.controller';
export const datatableSortIcon = {
bindings: {
key: '@',
selectedSortKey: '@',
reverseOrder: '<',
},
controller,
templateUrl: './datatable-sort-icon.html',
};

View File

@@ -0,0 +1,7 @@
<div class="toolBar">
<div class="toolBarTitle">
<i class="fa" ng-class="$ctrl.icon" aria-hidden="true" style="margin-right: 2px;"></i>
<span style="margin-right: 10px;">{{ $ctrl.title }}</span>
<be-feature-indicator feature="$ctrl.feature"></be-feature-indicator>
</div>
</div>

View File

@@ -0,0 +1,8 @@
export const datatableTitlebar = {
bindings: {
icon: '@',
title: '@',
feature: '@',
},
templateUrl: './datatable-titlebar.html',
};

View File

@@ -10,5 +10,7 @@
ng-model="$ctrl.ngModel"
disabled="$ctrl.disabled"
on-change="($ctrl.onChange)"
feature="$ctrl.feature"
ng-data-cy="{{::$ctrl.ngDataCy}}"
></por-switch>
</label>

View File

@@ -1,7 +1,5 @@
import angular from 'angular';
import './por-switch-field.css';
export const porSwitchField = {
templateUrl: './por-switch-field.html',
bindings: {
@@ -10,8 +8,10 @@ export const porSwitchField = {
label: '@',
name: '@',
labelClass: '@',
ngDataCy: '@',
disabled: '<',
onChange: '<',
feature: '<', // feature id
},
};

View File

@@ -0,0 +1,14 @@
export default class PorSwitchController {
/* @ngInject */
constructor(featureService) {
Object.assign(this, { featureService });
this.limitedToBE = false;
}
$onInit() {
if (this.feature) {
this.limitedToBE = this.featureService.isLimitedToBE(this.feature);
}
}
}

View File

@@ -51,3 +51,18 @@
opacity: 0.5;
cursor: not-allowed;
}
.switch.limited {
pointer-events: none;
touch-action: none;
}
.switch.limited i {
opacity: 1;
cursor: not-allowed;
}
.switch.business i {
background-color: var(--BE-only);
box-shadow: inset 0 0 1px rgb(0 0 0 / 50%), inset 0 0 40px var(--BE-only);
}

View File

@@ -1,4 +1,12 @@
<label class="switch" ng-class="$ctrl.className" style="margin-bottom: 0;">
<input type="checkbox" name="{{::$ctrl.name}}" id="{{::$ctrl.id}}" ng-model="$ctrl.ngModel" ng-disabled="$ctrl.disabled" ng-change="$ctrl.onChange($ctrl.ngModel)" />
<i></i>
<label class="switch" ng-class="[$ctrl.className, { business: $ctrl.limitedToBE, limited: $ctrl.limitedToBE }]" style="margin-bottom: 0;">
<input
type="checkbox"
name="{{::$ctrl.name}}"
id="{{::$ctrl.id}}"
ng-model="$ctrl.ngModel"
ng-disabled="$ctrl.disabled || $ctrl.limitedToBE"
ng-change="$ctrl.onChange($ctrl.ngModel)"
/>
<i data-cy="{{::$ctrl.ngDataCy}}"></i>
</label>
<be-feature-indicator ng-if="$ctrl.limitedToBE" feature="$ctrl.feature"></be-feature-indicator>

View File

@@ -1,14 +1,20 @@
import angular from 'angular';
import controller from './por-switch.controller';
import './por-switch.css';
const porSwitch = {
templateUrl: './por-switch.html',
controller,
bindings: {
ngModel: '=',
id: '@',
className: '@',
name: '@',
ngDataCy: '@',
disabled: '<',
onChange: '<',
feature: '<', // feature id
},
};

View File

@@ -6,9 +6,20 @@ angular.module('portainer.app').directive('rdWidgetHeader', function rdWidgetTit
icon: '@',
classes: '@?',
},
transclude: true,
template:
'<div class="widget-header"><div class="row"><span ng-class="classes" class="pull-left"><i class="fa" ng-class="icon"></i> {{titleText}} </span><span ng-class="classes" class="pull-right" ng-transclude></span></div></div>',
transclude: {
title: '?headerTitle',
},
template: `
<div class="widget-header">
<div class="row">
<span ng-class="classes" class="pull-left">
<i class="fa" ng-class="icon"></i>
<span ng-transclude="title">{{ titleText }}</span>
</span>
<span ng-class="classes" class="pull-right" ng-transclude></span>
</div>
</div>
`,
restrict: 'E',
};
return directive;

View File

@@ -0,0 +1,10 @@
export const EDITIONS = {
CE: 0,
BE: 1,
};
export const STATES = {
HIDDEN: 0,
VISIBLE: 1,
LIMITED_BE: 2,
};

View File

@@ -0,0 +1,26 @@
.form-control.limited-be {
border-color: var(--BE-only);
}
.form-control.limited-be.no-border {
border-color: var(--border-form-control-color);
}
button.limited-be {
background-color: var(--BE-only);
border-color: var(--BE-only);
}
ng-form.limited-be,
form.limited-be,
div.limited-be {
border: solid 1px var(--BE-only);
padding: 10px;
pointer-events: none;
touch-action: none;
display: block;
}
.form-control.limited-be[disabled] {
background-color: transparent !important;
}

View File

@@ -0,0 +1,57 @@
import { EDITIONS, STATES } from './enums';
import * as FEATURE_IDS from './feature-ids';
export function featureService() {
const state = {
currentEdition: undefined,
features: {},
};
return {
selectShow,
init,
isLimitedToBE,
};
async function init() {
// will be loaded on runtime
const currentEdition = EDITIONS.CE;
const features = {
[FEATURE_IDS.K8S_RESOURCE_POOL_LB_QUOTA]: EDITIONS.BE,
[FEATURE_IDS.K8S_RESOURCE_POOL_STORAGE_QUOTA]: EDITIONS.BE,
[FEATURE_IDS.ACTIVITY_AUDIT]: EDITIONS.BE,
[FEATURE_IDS.EXTERNAL_AUTH_LDAP]: EDITIONS.BE,
[FEATURE_IDS.HIDE_INTERNAL_AUTH]: EDITIONS.BE,
[FEATURE_IDS.HIDE_INTERNAL_AUTHENTICATION_PROMPT]: EDITIONS.BE,
[FEATURE_IDS.K8S_SETUP_DEFAULT]: EDITIONS.BE,
[FEATURE_IDS.RBAC_ROLES]: EDITIONS.BE,
[FEATURE_IDS.REGISTRY_MANAGEMENT]: EDITIONS.BE,
[FEATURE_IDS.S3_BACKUP_SETTING]: EDITIONS.BE,
[FEATURE_IDS.TEAM_MEMBERSHIP]: EDITIONS.BE,
};
state.currentEdition = currentEdition;
state.features = features;
}
function selectShow(featureId) {
if (!state.features[featureId]) {
return STATES.HIDDEN;
}
if (state.features[featureId] <= state.currentEdition) {
return STATES.VISIBLE;
}
if (state.features[featureId] === EDITIONS.BE) {
return STATES.LIMITED_BE;
}
return STATES.HIDDEN;
}
function isLimitedToBE(featureId) {
return selectShow(featureId) === STATES.LIMITED_BE;
}
}

View File

@@ -0,0 +1,11 @@
export const K8S_RESOURCE_POOL_LB_QUOTA = 'k8s-resourcepool-Ibquota';
export const K8S_RESOURCE_POOL_STORAGE_QUOTA = 'k8s-resourcepool-storagequota';
export const RBAC_ROLES = 'rbac-roles';
export const REGISTRY_MANAGEMENT = 'registry-management';
export const K8S_SETUP_DEFAULT = 'k8s-setup-default';
export const S3_BACKUP_SETTING = 's3-backup-setting';
export const HIDE_INTERNAL_AUTHENTICATION_PROMPT = 'hide-internal-authentication-prompt';
export const TEAM_MEMBERSHIP = 'team-membership';
export const HIDE_INTERNAL_AUTH = 'hide-internal-auth';
export const EXTERNAL_AUTH_LDAP = 'external-auth-ldap';
export const ACTIVITY_AUDIT = 'activity-audit';

View File

@@ -0,0 +1,7 @@
import angular from 'angular';
import { limitedFeatureDirective } from './limited-feature.directive';
import { featureService } from './feature-flags.service';
import './feature-flags.css';
export default angular.module('portainer.feature-flags', []).directive('limitedFeatureDir', limitedFeatureDirective).factory('featureService', featureService).name;

View File

@@ -0,0 +1,41 @@
import _ from 'lodash-es';
import { STATES } from '@/portainer/feature-flags/enums';
const BASENAME = 'limitedFeature';
/* @ngInject */
export function limitedFeatureDirective(featureService) {
return {
restrict: 'A',
link,
};
function link(scope, elem, attrs) {
const { limitedFeatureDir: featureId } = attrs;
if (!featureId) {
return;
}
const limitedFeatureAttrs = Object.keys(attrs)
.filter((attr) => attr.startsWith(BASENAME) && attr !== `${BASENAME}Dir`)
.map((attr) => [_.kebabCase(attr.replace(BASENAME, '')), attrs[attr]]);
const state = featureService.selectShow(featureId);
if (state === STATES.HIDDEN) {
elem.hide();
return;
}
if (state === STATES.VISIBLE) {
return;
}
limitedFeatureAttrs.forEach(([attr, value = attr]) => {
const currentValue = elem.attr(attr) || '';
elem.attr(attr, `${currentValue} ${value}`.trim());
});
}
}

View File

@@ -1,8 +1,11 @@
import angular from 'angular';
import controller from './oauth-provider-selector.controller';
angular.module('portainer.oauth').component('oauthProvidersSelector', {
templateUrl: './oauth-providers-selector.html',
bindings: {
onSelect: '<',
provider: '=',
onChange: '<',
value: '<',
},
controller: 'OAuthProviderSelectorController',
controller,
});

View File

@@ -1,39 +0,0 @@
angular.module('portainer.oauth').controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() {
var ctrl = this;
this.providers = [
{
authUrl: '',
accessTokenUrl: '',
resourceUrl: '',
userIdentifier: '',
scopes: '',
name: 'custom',
label: 'Custom',
description: 'Custom OAuth provider',
icon: 'fa fa-user-check',
},
];
this.$onInit = onInit;
function onInit() {
if (ctrl.provider.authUrl) {
ctrl.provider = getProviderByURL(ctrl.provider.authUrl);
} else {
ctrl.provider = ctrl.providers[0];
}
ctrl.onSelect(ctrl.provider, false);
}
function getProviderByURL(providerAuthURL) {
if (providerAuthURL.indexOf('login.microsoftonline.com') !== -1) {
return ctrl.providers[0];
} else if (providerAuthURL.indexOf('accounts.google.com') !== -1) {
return ctrl.providers[1];
} else if (providerAuthURL.indexOf('github.com') !== -1) {
return ctrl.providers[2];
}
return ctrl.providers[3];
}
});

View File

@@ -0,0 +1,14 @@
import { HIDE_INTERNAL_AUTH } from '@/portainer/feature-flags/feature-ids';
import { buildOption } from '@/portainer/components/box-selector';
export default class OAuthProviderSelectorController {
constructor() {
this.options = [
buildOption('microsoft', 'fab fa-microsoft', 'Microsoft', 'Microsoft OAuth provider', 'microsoft', HIDE_INTERNAL_AUTH),
buildOption('google', 'fab fa-google', 'Google', 'Google OAuth provider', 'google', HIDE_INTERNAL_AUTH),
buildOption('github', 'fab fa-github', 'Github', 'Github OAuth provider', 'github', HIDE_INTERNAL_AUTH),
buildOption('custom', 'fa fa-user-check', 'Custom', 'Custom OAuth provider', 'custom'),
];
}
}

View File

@@ -2,18 +2,4 @@
Provider
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0;">
<div class="boxselector_wrapper">
<div ng-repeat="provider in $ctrl.providers" ng-click="$ctrl.onSelect(provider, true)">
<input type="radio" id="{{ 'oauth_provider_' + provider.name }}" ng-model="$ctrl.provider" ng-value="provider" />
<label for="{{ 'oauth_provider_' + provider.name }}">
<div class="boxselector_header">
<i ng-class="provider.icon" aria-hidden="true" style="margin-right: 2px;"></i>
{{ provider.label }}
</div>
<p>{{ provider.description }}</p>
</label>
</div>
</div>
</div>
<box-selector ng-model="$ctrl.value" options="$ctrl.options" on-change="($ctrl.onChange)" radio-name="oauth_provider"></box-selector>

View File

@@ -1,8 +1,11 @@
import angular from 'angular';
import controller from './oauth-settings.controller';
angular.module('portainer.oauth').component('oauthSettings', {
templateUrl: './oauth-settings.html',
bindings: {
settings: '=',
teams: '<',
},
controller: 'OAuthSettingsController',
controller,
});

View File

@@ -1,23 +0,0 @@
angular.module('portainer.oauth').controller('OAuthSettingsController', function OAuthSettingsController() {
var ctrl = this;
this.state = {
provider: {},
};
this.$onInit = $onInit;
function $onInit() {
if (ctrl.settings.RedirectURI === '') {
ctrl.settings.RedirectURI = window.location.origin;
}
if (ctrl.settings.AuthorizationURI !== '') {
ctrl.state.provider.authUrl = ctrl.settings.AuthorizationURI;
}
if (ctrl.settings.DefaultTeamID === 0) {
ctrl.settings.DefaultTeamID = null;
}
}
});

View File

@@ -0,0 +1,109 @@
import { HIDE_INTERNAL_AUTH } from '@/portainer/feature-flags/feature-ids';
import providers, { getProviderByUrl } from './providers';
export default class OAuthSettingsController {
/* @ngInject */
constructor(featureService) {
this.featureService = featureService;
this.limitedFeature = HIDE_INTERNAL_AUTH;
this.state = {
provider: 'custom',
overrideConfiguration: false,
microsoftTenantID: '',
};
this.$onInit = this.$onInit.bind(this);
this.onSelectProvider = this.onSelectProvider.bind(this);
this.onMicrosoftTenantIDChange = this.onMicrosoftTenantIDChange.bind(this);
this.useDefaultProviderConfiguration = this.useDefaultProviderConfiguration.bind(this);
this.updateSSO = this.updateSSO.bind(this);
this.addTeamMembershipMapping = this.addTeamMembershipMapping.bind(this);
this.removeTeamMembership = this.removeTeamMembership.bind(this);
}
onMicrosoftTenantIDChange() {
const tenantID = this.state.microsoftTenantID;
this.settings.AuthorizationURI = `https://login.microsoftonline.com/${tenantID}/oauth2/authorize`;
this.settings.AccessTokenURI = `https://login.microsoftonline.com/${tenantID}/oauth2/token`;
this.settings.ResourceURI = `https://graph.windows.net/${tenantID}/me?api-version=2013-11-08`;
}
useDefaultProviderConfiguration(providerId) {
const provider = providers[providerId];
this.state.overrideConfiguration = false;
if (!this.isLimitedToBE || providerId === 'custom') {
this.settings.AuthorizationURI = provider.authUrl;
this.settings.AccessTokenURI = provider.accessTokenUrl;
this.settings.ResourceURI = provider.resourceUrl;
this.settings.LogoutURI = provider.logoutUrl;
this.settings.UserIdentifier = provider.userIdentifier;
this.settings.Scopes = provider.scopes;
if (providerId === 'microsoft' && this.state.microsoftTenantID !== '') {
this.onMicrosoftTenantIDChange();
}
} else {
this.settings.ClientID = '';
this.settings.ClientSecret = '';
}
}
onSelectProvider(provider) {
this.state.provider = provider;
this.useDefaultProviderConfiguration(provider);
}
updateSSO() {
this.settings.HideInternalAuth = this.settings.SSO;
}
addTeamMembershipMapping() {
this.settings.TeamMemberships.OAuthClaimMappings.push({ ClaimValRegex: '', Team: this.settings.DefaultTeamID });
}
removeTeamMembership(index) {
this.settings.TeamMemberships.OAuthClaimMappings.splice(index, 1);
}
$onInit() {
this.isLimitedToBE = this.featureService.isLimitedToBE(this.limitedFeature);
if (this.isLimitedToBE) {
return;
}
if (this.settings.RedirectURI === '') {
this.settings.RedirectURI = window.location.origin;
}
if (this.settings.AuthorizationURI) {
const authUrl = this.settings.AuthorizationURI;
this.state.provider = getProviderByUrl(authUrl);
if (this.state.provider === 'microsoft') {
const tenantID = authUrl.match(/login.microsoftonline.com\/(.*?)\//)[1];
this.state.microsoftTenantID = tenantID;
this.onMicrosoftTenantIDChange();
}
}
if (this.settings.DefaultTeamID === 0) {
this.settings.DefaultTeamID = null;
}
if (this.settings.TeamMemberships == null) {
this.settings.TeamMemberships = {};
}
if (this.settings.TeamMemberships.OAuthClaimMappings === null) {
this.settings.TeamMemberships.OAuthClaimMappings = [];
}
}
}

View File

@@ -1,54 +1,50 @@
<div
><div class="col-sm-12 form-section-title">
<ng-form name="$ctrl.oauthSettingsForm">
<div class="col-sm-12 form-section-title">
Single Sign-On
</div>
<!-- SSO -->
<div class="form-group">
<label for="use-sso" class="control-label col-sm-2 text-left" style="padding-top: 0;">
Use SSO
<portainer-tooltip position="bottom" message="When using SSO the OAuth provider is not forced to prompt for credentials."></portainer-tooltip>
</label>
<div class="col-sm-9">
<label class="switch"> <input id="use-sso" type="checkbox" ng-model="$ctrl.settings.SSO" /><i></i> </label>
<div class="col-sm-12">
<por-switch-field
label="Use SSO"
tooltip="When using SSO the OAuth provider is not forced to prompt for credentials."
name="use-sso"
ng-model="$ctrl.settings.SSO"
on-change="$ctrl.updateSSO()"
></por-switch-field>
</div>
</div>
<!-- !SSO -->
<!-- HideInternalAuth -->
<div class="form-group" ng-if="$ctrl.settings.SSO">
<label for="hide-internal-auth" class="control-label col-sm-2 text-left" style="padding-top: 0;">
Hide internal authentication prompt
</label>
<div class="col-sm-9">
<label class="switch"> <input id="hide-internal-auth" type="checkbox" disabled /><i></i> </label>
<span class="text-muted small" style="margin-left: 15px;">
This feature is available in <a href="https://www.portainer.io/business-upsell?from=hide-internal-auth" target="_blank"> Portainer Business Edition</a>.
</span>
<div class="col-sm-12">
<por-switch-field
label="Hide internal authentication prompt"
name="hide-internal-auth"
feature="$ctrl.limitedFeature"
ng-model="$ctrl.settings.HideInternalAuth"
></por-switch-field>
</div>
</div>
<!-- !HideInternalAuth -->
<div class="col-sm-12 form-section-title">
Automatic user provisioning
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<auto-user-provision-toggle ng-model="$ctrl.settings.OAuthAutoCreateUsers">
<field-description>
With automatic user provisioning enabled, Portainer will create user(s) automatically with the standard user role. If disabled, users must be created beforehand in Portainer
in order to login.
</span>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">Automatic user provisioning</label>
<label class="switch" style="margin-left: 20px;"> <input type="checkbox" ng-model="$ctrl.settings.OAuthAutoCreateUsers" /><i></i> </label>
</div>
</field-description>
</auto-user-provision-toggle>
<div ng-if="$ctrl.settings.OAuthAutoCreateUsers">
<div class="form-group">
<span class="col-sm-12 text-muted small">
<p>The users created by the automatic provisioning feature can be added to a default team on creation.</p>
<p
>By assigning newly created users to a team, they will be able to access the environments associated to that team. This setting is optional and if not set, newly created
users won't be able to access any environments.</p
>
<p>
By assigning newly created users to a team, they will be able to access the environments associated to that team. This setting is optional and if not set, newly created
users won't be able to access any environments.
</p>
</span>
</div>
<div class="form-group">
@@ -56,13 +52,33 @@
<span class="small text-muted" style="margin-left: 20px;" ng-if="$ctrl.teams.length === 0">
You have not yet created any teams. Head over to the <a ui-sref="portainer.teams">Teams view</a> to manage teams.
</span>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.settings.DefaultTeamID = null" ng-disabled="!$ctrl.settings.DefaultTeamID" ng-if="$ctrl.teams.length > 0"
><i class="fa fa-times" aria-hidden="true"></i
></button>
<div class="col-sm-9 col-lg-9" ng-if="$ctrl.teams.length > 0">
<select class="form-control" ng-model="$ctrl.settings.DefaultTeamID" ng-options="team.Id as team.Name for team in $ctrl.teams">
<option value="">No team</option>
</select>
<div class="col-sm-9" ng-if="$ctrl.teams.length > 0">
<div class="col-sm-12 small text-muted">
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
The default team option will be disabled when automatic team membership is enabled
</p>
</div>
<div class="col-xs-11">
<select
class="form-control"
ng-disabled="$ctrl.settings.OAuthAutoMapTeamMemberships"
ng-model="$ctrl.settings.DefaultTeamID"
ng-options="team.Id as team.Name for team in $ctrl.teams"
>
<option value="">No team</option>
</select>
</div>
<button
type="button"
class="btn btn-sm btn-danger"
ng-click="$ctrl.settings.DefaultTeamID = null"
ng-disabled="!$ctrl.settings.DefaultTeamID || $ctrl.settings.OAuthAutoMapTeamMemberships"
ng-if="$ctrl.teams.length > 0"
>
<i class="fa fa-times" aria-hidden="true"> </i
></button>
</div>
</div>
</div>
@@ -71,118 +87,296 @@
Team membership
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
<div class="col-sm-12 text-muted small">
Automatic team membership synchronizes the team membership based on a custom claim in the token from the OAuth provider.
</span>
</div>
</div>
<div class="form-group">
<span class="text-muted small" style="margin-left: 15px;">
<i class="fa fa-user" aria-hidden="true"></i>
This feature is available in <a href="https://www.portainer.io/business-upsell?from=oauth-group-membership" target="_blank"> Portainer Business Edition</a>.
</span>
<div class="col-sm-12">
<por-switch-field label="Automatic team membership" name="tls" feature="$ctrl.limitedFeature" ng-model="$ctrl.settings.OAuthAutoMapTeamMemberships"></por-switch-field>
</div>
</div>
<div ng-if="$ctrl.settings.OAuthAutoMapTeamMemberships">
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">
Claim name
<portainer-tooltip position="bottom" message="The OpenID Connect UserInfo Claim name that contains the team identifier the user belongs to."></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<div class="col-xs-11 col-lg-10">
<input type="text" class="form-control" id="oauth_token_claim_name" ng-model="$ctrl.settings.TeamMemberships.OAuthClaimName" placeholder="groups" />
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">
Statically assigned teams
</label>
<div class="col-sm-9 col-lg-10">
<span class="label label-default interactive" style="margin-left: 1.4em;" ng-click="$ctrl.addTeamMembershipMapping()">
<i class="fa fa-plus-circle" aria-hidden="true"></i> add team mapping
</span>
<div class="col-sm-12 form-inline" ng-repeat="mapping in $ctrl.settings.TeamMemberships.OAuthClaimMappings" style="margin-top: 0.75em;">
<div class="input-group input-group-sm col-sm-5">
<span class="input-group-addon">claim value regex</span>
<input type="text" class="form-control" ng-model="mapping.ClaimValRegex" />
</div>
<span style="margin: 0px 0.5em;">maps to</span>
<div class="input-group input-group-sm col-sm-3 col-lg-4">
<span class="input-group-addon">team</span>
<select
class="form-control"
ng-init="mapping.Team = mapping.Team || $ctrl.settings.DefaultTeamID"
ng-model="mapping.Team"
ng-options="team.Id as team.Name for team in $ctrl.teams"
>
<option selected value="">Select a team</option>
</select>
</div>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.removeTeamMembership($index)"> <i class="fa fa-trash" aria-hidden="true"> </i></button>
<div class="small text-warning" ng-show="!mapping.ClaimValRegex" style="margin-top: 0.4em;">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Claim value regex is required.
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-12 text-muted small" style="margin-bottom: 0.5em;">
The default team will be assigned when the user does not belong to any other team
</div>
<label class="col-sm-3 col-lg-2 control-label text-left">Default team</label>
<span class="small text-muted" style="margin-left: 20px;" ng-if="$ctrl.teams.length === 0">
You have not yet created any teams. Head over to the <a ui-sref="portainer.teams">Teams view</a> to manage teams.
</span>
<div class="col-sm-9" ng-if="$ctrl.teams.length > 0">
<div class="col-xs-11">
<select class="form-control" ng-model="$ctrl.settings.DefaultTeamID" ng-options="team.Id as team.Name for team in $ctrl.teams">
<option value="">No team</option>
</select>
</div>
</div>
</div>
</div>
<oauth-providers-selector on-change="($ctrl.onSelectProvider)" value="$ctrl.state.provider"></oauth-providers-selector>
<div class="col-sm-12 form-section-title">OAuth Configuration</div>
<div class="form-group" ng-if="$ctrl.state.provider == 'microsoft'">
<label for="oauth_microsoft_tenant_id" class="col-sm-3 col-lg-2 control-label text-left">
Tenant ID
<portainer-tooltip position="bottom" message="ID of the Azure Directory you wish to authenticate against. Also known as the Directory ID"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_microsoft_tenant_id"
placeholder="xxxxxxxxxxxxxxxxxxxx"
ng-model="$ctrl.state.microsoftTenantID"
ng-change="$ctrl.onMicrosoftTenantIDChange()"
limited-feature-dir="{{::$ctrl.limitedFeature}}"
limited-feature-class="limited-be"
limited-feature-disabled
limited-feature-tabindex="-1"
required
/>
</div>
</div>
<div class="form-group">
<label for="oauth_client_id" class="col-sm-3 col-lg-2 control-label text-left">
Client ID
{{ $ctrl.state.provider == 'microsoft' ? 'Application ID' : 'Client ID' }}
<portainer-tooltip position="bottom" message="Public identifier of the OAuth application"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_client_id" ng-model="$ctrl.settings.ClientID" placeholder="xxxxxxxxxxxxxxxxxxxx" />
<input
type="text"
id="oauth_client_id"
ng-model="$ctrl.settings.ClientID"
placeholder="xxxxxxxxxxxxxxxxxxxx"
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
tabindex="{{ $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' ? -1 : 0 }}"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_client_secret" class="col-sm-3 col-lg-2 control-label text-left">
Client secret
{{ $ctrl.state.provider == 'microsoft' ? 'Application key' : 'Client secret' }}
</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" id="oauth_client_secret" ng-model="$ctrl.settings.ClientSecret" placeholder="xxxxxxxxxxxxxxxxxxxx" autocomplete="new-password" />
<input
type="password"
class="form-control"
id="oauth_client_secret"
ng-model="$ctrl.settings.ClientSecret"
placeholder="xxxxxxxxxxxxxxxxxxxx"
autocomplete="new-password"
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
tabindex="{{ $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' ? -1 : 0 }}"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_authorization_uri" class="col-sm-3 col-lg-2 control-label text-left">
Authorization URL
<portainer-tooltip
position="bottom"
message="URL used to authenticate against the OAuth provider. Will redirect the user to the OAuth provider login view"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_authorization_uri" ng-model="$ctrl.settings.AuthorizationURI" placeholder="https://example.com/oauth/authorize" />
<div ng-if="$ctrl.state.provider == 'custom' || $ctrl.state.overrideConfiguration">
<div class="form-group">
<label for="oauth_authorization_uri" class="col-sm-3 col-lg-2 control-label text-left">
Authorization URL
<portainer-tooltip
position="bottom"
message="URL used to authenticate against the OAuth provider. Will redirect the user to the OAuth provider login view"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_authorization_uri"
ng-model="$ctrl.settings.AuthorizationURI"
placeholder="https://example.com/oauth/authorize"
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_access_token_uri" class="col-sm-3 col-lg-2 control-label text-left">
Access token URL
<portainer-tooltip position="bottom" message="URL used by Portainer to exchange a valid OAuth authentication code for an access token"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_access_token_uri"
ng-model="$ctrl.settings.AccessTokenURI"
placeholder="https://example.com/oauth/token"
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_resource_uri" class="col-sm-3 col-lg-2 control-label text-left">
Resource URL
<portainer-tooltip position="bottom" message="URL used by Portainer to retrieve information about the authenticated user"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_resource_uri"
ng-model="$ctrl.settings.ResourceURI"
placeholder="https://example.com/user"
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_redirect_uri" class="col-sm-3 col-lg-2 control-label text-left">
Redirect URL
<portainer-tooltip
position="bottom"
message="URL used by the OAuth provider to redirect the user after successful authentication. Should be set to your Portainer instance URL"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_redirect_uri"
ng-model="$ctrl.settings.RedirectURI"
placeholder="http://yourportainer.com/"
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_logout_url" class="col-sm-3 col-lg-2 control-label text-left">
Logout URL
<portainer-tooltip
position="bottom"
message="URL used by Portainer to redirect the user to the OAuth provider in order to log the user out of the identity provider session."
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_logout_url"
ng-model="$ctrl.settings.LogoutURI"
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
User identifier
<portainer-tooltip
position="bottom"
message="Identifier that will be used by Portainer to create an account for the authenticated user. Retrieved from the resource server specified via the Resource URL field"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_user_identifier"
ng-model="$ctrl.settings.UserIdentifier"
placeholder="id"
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_scopes" class="col-sm-3 col-lg-2 control-label text-left">
Scopes
<portainer-tooltip
position="bottom"
message="Scopes required by the OAuth provider to retrieve information about the authenticated user. Refer to your OAuth provider documentation for more information about this"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_scopes"
ng-model="$ctrl.settings.Scopes"
placeholder="id,email,name"
ng-class="['form-control', { 'limited-be': $ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom' }]"
ng-disabled="$ctrl.isLimitedToBE && $ctrl.state.provider !== 'custom'"
/>
</div>
</div>
</div>
<div class="form-group">
<label for="oauth_access_token_uri" class="col-sm-3 col-lg-2 control-label text-left">
Access token URL
<portainer-tooltip position="bottom" message="URL used by Portainer to exchange a valid OAuth authentication code for an access token"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_access_token_uri" ng-model="$ctrl.settings.AccessTokenURI" placeholder="https://example.com/oauth/token" />
<div class="form-group" ng-if="$ctrl.state.provider != 'custom'">
<div class="col-sm-12">
<a class="small interactive" ng-if="!$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = true;">
<i class="fa fa-wrench space-right" aria-hidden="true"></i> Override default configuration
</a>
<a class="small interactive" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.useDefaultProviderConfiguration($ctrl.state.provider)">
<i class="fa fa-cogs space-right" aria-hidden="true"></i> Use default configuration
</a>
</div>
</div>
<div class="form-group">
<label for="oauth_resource_uri" class="col-sm-3 col-lg-2 control-label text-left">
Resource URL
<portainer-tooltip position="bottom" message="URL used by Portainer to retrieve information about the authenticated user"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_resource_uri" ng-model="$ctrl.settings.ResourceURI" placeholder="https://example.com/user" />
</div>
</div>
<div class="form-group">
<label for="oauth_redirect_uri" class="col-sm-3 col-lg-2 control-label text-left">
Redirect URL
<portainer-tooltip
position="bottom"
message="URL used by the OAuth provider to redirect the user after successful authentication. Should be set to your Portainer instance URL"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_redirect_uri" ng-model="$ctrl.settings.RedirectURI" placeholder="http://yourportainer.com/" />
</div>
</div>
<div class="form-group">
<label for="oauth_logout_url" class="col-sm-3 col-lg-2 control-label text-left">
Logout URL
<portainer-tooltip
position="bottom"
message="URL used by Portainer to redirect the user to the OAuth provider in order to log the user out of the identity provider session."
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_logout_url" ng-model="$ctrl.settings.LogoutURI" />
</div>
</div>
<div class="form-group">
<label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
User identifier
<portainer-tooltip
position="bottom"
message="Identifier that will be used by Portainer to create an account for the authenticated user. Retrieved from the resource server specified via the Resource URL field"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_user_identifier" ng-model="$ctrl.settings.UserIdentifier" placeholder="id" />
</div>
</div>
<div class="form-group">
<label for="oauth_scopes" class="col-sm-3 col-lg-2 control-label text-left">
Scopes
<portainer-tooltip
position="bottom"
message="Scopes required by the OAuth provider to retrieve information about the authenticated user. Refer to your OAuth provider documentation for more information about this"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input type="text" class="form-control" id="oauth_scopes" ng-model="$ctrl.settings.Scopes" placeholder="id,email,name" />
</div>
</div>
</div>
</ng-form>

View File

@@ -0,0 +1,43 @@
export default {
microsoft: {
authUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/authorize',
accessTokenUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/token',
resourceUrl: 'https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08',
logoutUrl: `https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=${window.location.origin}/#!/auth`,
userIdentifier: 'userPrincipalName',
scopes: 'id,email,name',
},
google: {
authUrl: 'https://accounts.google.com/o/oauth2/auth',
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
logoutUrl: `https://www.google.com/accounts/Logout?continue=https://appengine.google.com/_ah/logout?continue=${window.location.origin}/#!/auth`,
userIdentifier: 'email',
scopes: 'profile email',
},
github: {
authUrl: 'https://github.com/login/oauth/authorize',
accessTokenUrl: 'https://github.com/login/oauth/access_token',
resourceUrl: 'https://api.github.com/user',
logoutUrl: `https://github.com/logout`,
userIdentifier: 'login',
scopes: 'id email name',
},
custom: { authUrl: '', accessTokenUrl: '', resourceUrl: '', logoutUrl: '', userIdentifier: '', scopes: '' },
};
export function getProviderByUrl(providerAuthURL = '') {
if (providerAuthURL.includes('login.microsoftonline.com')) {
return 'microsoft';
}
if (providerAuthURL.includes('accounts.google.com')) {
return 'google';
}
if (providerAuthURL.includes('github.com')) {
return 'github';
}
return 'custom';
}

View File

@@ -0,0 +1,73 @@
<div class="datatable">
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." ng-model-options="{ debounce: 300 }" />
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<thead>
<tr>
<th>
<a ng-click="$ctrl.changeOrderBy('EndpointName')">
Environment
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'EndpointName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'EndpointName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('RoleName')">
Role
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'RoleName' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>Access origin</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit)) track by $index"
>
<td>{{ item.EndpointName }}</td>
<td>{{ item.RoleName }}</td>
<td
>{{ item.TeamName ? 'Team' : 'User' }} <code ng-if="item.TeamName">{{ item.TeamName }}</code> access defined on {{ item.AccessLocation }}
<code ng-if="item.GroupName">{{ item.GroupName }}</code>
<a ng-if="item.AccessLocation === 'endpoint'" ui-sref="portainer.endpoints.endpoint.access({id: item.EndpointId})"
><i style="margin-left: 5px;" class="fa fa-users" aria-hidden="true"></i> Manage access
</a>
<a ng-if="item.AccessLocation === 'endpoint group'" ui-sref="portainer.groups.group.access({id: item.GroupId})"
><i style="margin-left: 5px;" class="fa fa-users" aria-hidden="true"></i> Manage access
</a>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Select a user to show associated access and role</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">The selected user does not have access to any environment(s)</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
angular.module('portainer.app').component('accessViewerDatatable', {
templateUrl: './accessViewerDatatable.html',
export const accessViewerDatatable = {
templateUrl: './access-viewer-datatable.html',
controller: 'GenericDatatableController',
bindings: {
titleText: '@',
@@ -8,4 +8,4 @@ angular.module('portainer.app').component('accessViewerDatatable', {
orderBy: '@',
dataset: '<',
},
});
};

View File

@@ -0,0 +1,128 @@
import _ from 'lodash-es';
import AccessViewerPolicyModel from '../../models/access';
export default class AccessViewerController {
/* @ngInject */
constructor(featureService, Notifications, RoleService, UserService, EndpointService, GroupService, TeamService, TeamMembershipService) {
this.featureService = featureService;
this.Notifications = Notifications;
this.RoleService = RoleService;
this.UserService = UserService;
this.EndpointService = EndpointService;
this.GroupService = GroupService;
this.TeamService = TeamService;
this.TeamMembershipService = TeamMembershipService;
this.limitedFeature = 'rbac-roles';
this.users = [];
}
onUserSelect() {
this.userRoles = [];
const userRoles = {};
const user = this.selectedUser;
const userMemberships = _.filter(this.teamMemberships, { UserId: user.Id });
for (const [, endpoint] of _.entries(this.endpoints)) {
let role = this.getRoleFromUserEndpointPolicy(user, endpoint);
if (role) {
userRoles[endpoint.Id] = role;
continue;
}
role = this.getRoleFromUserEndpointGroupPolicy(user, endpoint);
if (role) {
userRoles[endpoint.Id] = role;
continue;
}
role = this.getRoleFromTeamEndpointPolicies(userMemberships, endpoint);
if (role) {
userRoles[endpoint.Id] = role;
continue;
}
role = this.getRoleFromTeamEndpointGroupPolicies(userMemberships, endpoint);
if (role) {
userRoles[endpoint.Id] = role;
}
}
this.userRoles = _.values(userRoles);
}
findLowestRole(policies) {
return _.first(_.orderBy(policies, 'RolePriority', 'desc'));
}
getRoleFromUserEndpointPolicy(user, endpoint) {
const policyRoles = [];
const policy = (endpoint.UserAccessPolicies || {})[user.Id];
if (policy) {
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, null, null);
policyRoles.push(accessPolicy);
}
return this.findLowestRole(policyRoles);
}
getRoleFromUserEndpointGroupPolicy(user, endpoint) {
const policyRoles = [];
const policy = this.groupUserAccessPolicies[endpoint.GroupId][user.Id];
if (policy) {
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, this.groups[endpoint.GroupId], null);
policyRoles.push(accessPolicy);
}
return this.findLowestRole(policyRoles);
}
getRoleFromTeamEndpointPolicies(memberships, endpoint) {
const policyRoles = [];
for (const membership of memberships) {
const policy = (endpoint.TeamAccessPolicies || {})[membership.TeamId];
if (policy) {
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, null, this.teams[membership.TeamId]);
policyRoles.push(accessPolicy);
}
}
return this.findLowestRole(policyRoles);
}
getRoleFromTeamEndpointGroupPolicies(memberships, endpoint) {
const policyRoles = [];
for (const membership of memberships) {
const policy = this.groupTeamAccessPolicies[endpoint.GroupId][membership.TeamId];
if (policy) {
const accessPolicy = new AccessViewerPolicyModel(policy, endpoint, this.roles, this.groups[endpoint.GroupId], this.teams[membership.TeamId]);
policyRoles.push(accessPolicy);
}
}
return this.findLowestRole(policyRoles);
}
async $onInit() {
try {
const limitedToBE = this.featureService.isLimitedToBE(this.limitedFeature);
if (limitedToBE) {
return;
}
this.users = await this.UserService.users();
this.endpoints = _.keyBy((await this.EndpointService.endpoints()).value, 'Id');
const groups = await this.GroupService.groups();
this.groupUserAccessPolicies = {};
this.groupTeamAccessPolicies = {};
_.forEach(groups, (group) => {
this.groupUserAccessPolicies[group.Id] = group.UserAccessPolicies;
this.groupTeamAccessPolicies[group.Id] = group.TeamAccessPolicies;
});
this.groups = _.keyBy(groups, 'Id');
this.roles = _.keyBy(await this.RoleService.roles(), 'Id');
this.teams = _.keyBy(await this.TeamService.teams(), 'Id');
this.teamMemberships = await this.TeamMembershipService.memberships();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retrieve accesses');
}
}
}

View File

@@ -0,0 +1,43 @@
<div class="col-sm-12" style="margin-bottom: 0px;">
<rd-widget>
<rd-widget-header icon="fa-user-lock">
<header-title>
Effective access viewer
<be-feature-indicator feature="$ctrl.limitedFeature" class="space-left"></be-feature-indicator>
</header-title>
</rd-widget-header>
<rd-widget-body>
<form class="form-horizontal">
<div class="col-sm-12 form-section-title">
User
</div>
<div class="form-group">
<div class="col-sm-12">
<span class="small text-muted" ng-if="$ctrl.users.length === 0">
No user available
</span>
<ui-select ng-if="$ctrl.users.length > 0" ng-model="$ctrl.selectedUser" ng-change="$ctrl.onUserSelect()">
<ui-select-match placeholder="Select a user">
<span>{{ $select.selected.Username }}</span>
</ui-select-match>
<ui-select-choices repeat="item in ($ctrl.users | filter: $select.search)">
<span>{{ item.Username }}</span>
</ui-select-choices>
</ui-select>
</div>
</div>
<div class="col-sm-12 form-section-title">
Access
</div>
<div>
<div class="small text-muted" style="margin-bottom: 15px;">
<i class="fa fa-info-circle blue-icon space-right" aria-hidden="true"></i>
Effective role for each environment will be displayed for the selected user
</div>
</div>
<access-viewer-datatable table-key="access_viewer" dataset="$ctrl.userRoles" order-by="EndpointName"> </access-viewer-datatable>
</form>
</rd-widget-body>
</rd-widget>
</div>

View File

@@ -0,0 +1,6 @@
import controller from './access-viewer.controller';
export const accessViewer = {
templateUrl: './access-viewer.html',
controller,
};

View File

@@ -0,0 +1,15 @@
import controller from './roles-datatable.controller';
import './roles-datatable.css';
export const rolesDatatable = {
templateUrl: './roles-datatable.html',
controller,
bindings: {
titleText: '@',
titleIcon: '@',
dataset: '<',
tableKey: '@',
orderBy: '@',
reverseOrder: '<',
},
};

View File

@@ -0,0 +1,15 @@
import angular from 'angular';
import { RoleTypes } from '../../models/role';
export default class RolesDatatableController {
/* @ngInject */
constructor($controller, $scope) {
this.limitedFeature = 'rbac-roles';
angular.extend(this, $controller('GenericDatatableController', { $scope }));
}
isDefaultItem(item) {
return item.ID === RoleTypes.STANDARD;
}
}

View File

@@ -0,0 +1,7 @@
th.be-visual-indicator-col {
width: 300px;
}
td.be-visual-indicator-col {
text-align: center;
}

View File

@@ -0,0 +1,85 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar">
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }} </div>
</div>
<div class="searchBar">
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
auto-focus
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="table-responsive">
<table class="table table-hover nowrap-cells">
<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>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('Description')">
Description
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Description' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Description' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th class="be-visual-indicator-col"></th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>{{ item.Name }}</td>
<td>{{ item.Description }}</td>
<td class="be-visual-indicator-col" ng-switch on="$ctrl.isDefaultItem(item)">
<span ng-switch-when="false">
<be-feature-indicator feature="$ctrl.limitedFeature"></be-feature-indicator>
</span>
<b ng-switch-when="true">Default</b>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-center text-muted">No role available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px;">
Items per page
</span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

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