Compare commits

...

163 Commits

Author SHA1 Message Date
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
zees-dev
8421113d49 portainer version updates (#5807) 2021-10-02 08:40:03 +13:00
Matt Hook
6bd72d21a8 fix(migration) datastore always marked new and migrations skipped EE-1775 (#5788)
* fix issue with broken store init

* minor logic improvement

* Remove fileexists logic as its redundant and handled implicitely by bolt.Open

* Added re-open test on IsNew flag.  Essential for migrations to be able to run
2021-10-01 20:35:43 +13:00
Chaim Lev-Ari
fc4ff59bfd fix(db): warn on missing docker id when migrating to db 31 (#5781)
* fix(db): warn on missing docker id when migrating to db 31

* fix(db): guard against nil exception
2021-10-01 15:27:39 +10: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
Luis Louis
cd651f2cba fix(template): Remove the no registry available on the registriesDataTable (#5774) 2021-10-01 18:15:32 +13:00
cong meng
328abfd74e fix(stack) normalize stack name EE-1701 (#5776)
* fix(stack) normalize stack name EE-1701

* fix(stack) normalize swarm stack name and fix rebase error EE-1701

* fix(stack) add front end stack name validation EE-1701

* fix(stack) make stack name regex as a const EE-1701

* fix(stack) reuse stack name regex for compose and swarm EE-1701

* fix(stack) add name validation for stack duplication form EE-1701

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-01 16:56:34 +13:00
Marcelo Rydel
fbcf67bc1e filter empty stacks in dropdown (#5771)
filter empty stacks in dropdown (#5771)
2021-09-30 09:32:38 -03:00
Chaim Lev-Ari
7fb2e44146 chore(build): set node_env to testing (#5410) 2021-09-30 12:00:54 +03:00
cong meng
0cb5656db6 feat(frontend) auto generate agent version EE-1266 (#5794)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-09-30 21:07:13 +13:00
Richard Wei
e4fd43e4fc fix icon line up issue in sidebar (#5790) 2021-09-30 18:23:13 +13:00
Richard Wei
34c2a16363 fix custom logo not updated (#5634) 2021-09-30 15:55:08 +13:00
Chaim Lev-Ari
0f33e4ae99 fix(wizard): align wizard grid (#5752)
* fix(wizard): align wizard grid [EE-1753]
2021-09-30 15:54:15 +13:00
Richard Wei
75071dfade feat(k8s): add filter for k8s application type EE-1627 (#5733)
* add filter for k8s application type
2021-09-30 15:53:03 +13:00
Richard Wei
34f6e11f1d fix showing create from application form when create from url (#5724) 2021-09-30 12:59:19 +13:00
Dmitry Salakhov
2ecc8ab5c9 feat(k8s): support git automated sync for k8s applications [EE-577] (#5548)
* feat(stack): backport changes to CE EE-1189

* feat(stack): front end backport changes to CE EE-1199 (#5455)

* feat(stack): front end backport changes to CE EE-1199

* fix k8s deploy logic

* fixed web editor confirmation message typo. EE-1501

* fix(stack): fixed issue auth detail not remembered EE-1502 (#5459)

* show status in buttons

* removed onChangeRef function.

* moved buttons in git form to its own component

* removed unused variable.

Co-authored-by: ArrisLee <arris_li@hotmail.com>

* moved formvalue to kube app component

* fix(stack): failed to pull and redeploy compose format k8s stack

* fixed form value

* fix(k8s): file content overridden when deployment failed with compose format EE-1548

* updated API response to get IsComposeFormat and show appropriate text.

* feat(k8s): front end backport to CE

* feat(kube): kube app auto update backend (#5547)

* error message updates for different file type

* not display creation source for external application

* added confirmation modal to advanced app created by web editor

* stop showing confirmation modal when updating application

* disable rollback button when application type is not applicatiom form

* only update file after deployment succeded

* Revert "only update file after deployment succeded"

This reverts commit b94bd2e96f.

* fix(k8s): file content overridden when deployment failed with compose format EE-1556

* added analytics-on directive to pull and redeploy button

* fix(kube): don't valide resource control access for kube (#5568)

* added missing question mark to k8s confirmation modal

* fixed webhook format issue

* added question marks to k8s app confirmation modal

* added space in additional file list.

* ignoring error on deletion

* fix(k8s): Git authentication info not persisted

* added RepositoryMechanismTypes constant

* updated analytics functions

* covert RepositoryMechanism to constant

* fixed typo

* removed unused function.

* post tech review updates

* fixed save settings n redeploy button

* refact kub deploy logic

* Revert "refact kub deploy logic"

This reverts commit cbfdd58ece.

* feat(k8s): utilize user token for k8s auto update EE-1594

* feat(k8s): persist kub stack name EE-1630

* feat(k8s): support delete kub stack

* fix(app): updated logic to delete stack for different kind apps. (#5648)

* fix(app): updated logic to delete stack for different kind apps.

* renamed variable

* fix import

* added StackName field.

* fixed stack id not found issue.

* fix(k8s): fixed qusetion mark alignment issue in PAT field. (#5611)

* fix(k8s): fixed qusetion mark alignment issue in PAT field.

* moved inline css to file.

* fix(git-form: made auth input text full width

* add ignore deleted arg

* tech review updates

* typo fix

* fix(k8s): added console error when deleting k8s service.

* fix(console): added no-console config

* fix(deploy): added missing service.

* fix: use stack editor as an owner when exists (#5678)

* fix: tempalte/content based stacks edit/delete

* fix(stack): remove stack when no app. (#5769)

* fix(stack): remove stack when no app.

* support compose format in delete

Co-authored-by: ArrisLee <arris_li@hotmail.com>

Co-authored-by: Hui <arris_li@hotmail.com>
Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
Co-authored-by: Felix Han <felix.han@portainer.io>
2021-09-30 12:58:10 +13:00
Marcelo Rydel
fce885901f fix(custom-templates): XSS issue in Custom Template Note <EE-1054> (#5766)
fix(custom-templates): XSS issue in Custom Template Note <EE-1054> (#5766)
2021-09-29 16:47:39 -03:00
Richard Wei
fe8f50512c set isolated as default for k8s app deploy (#5770) 2021-09-29 15:54:25 +13:00
zees-dev
e3b6e4a1d3 feat(configurations): portainer k8s configurations lingo update for explicitness EE-1626 (#5722)
* kubernetes sidebar configuration lingo updated

* configurations list view updated

* updated configurations list add config button

* - updated create and update configuration buttons to display type of configuration being created/updated
- configuration filter displays explicit configuration type

* updated create configuration sub-title

* add configmap wording update

* portainer service lingo updated in k8s app creation and update forms

* publishing mode text updates

* KubernetesApplicationPublishingTypes updated INTERNAL and CLUSTER to CLUSTER_IP and NODE_PORT respectively

* application ports datatable updated

* updated service and ingress lingo on application view page

* reduced spacing to fit in ConfigMaps & Secrets in sidenav for different screen res
2021-09-29 13:58:04 +13:00
Hui
01529203f1 fix(DB): modify new data store checking logic (#5756)
* update new data store check logic

* cleanup
2021-09-29 10:24:26 +10:00
zees-dev
af98660a55 feat(helm): helm apps deployed by portainer not marked as external EE-1624 (#5637)
* helm lib update

* helm handler requires kubernetes deployer to modify helm deployed resources

* AddAppLabels updated to be more generic - support for adding multiple labels using map

* path installed helm release manifest with portainer labels using kubectl

* updated helm handler unit tests to use mock KubernetesDeployer

* adding labels to manifest retrieved from release

* optional namespace support for k8s raw manifest deployment

* - inline postprocessing support when extracting
- get namespace from yaml support
- added and updated tests

* lowercase error wrapping

* updated libhelm dep
2021-09-29 10:12:45 +10:00
Chaim Lev-Ari
50f63ae865 feat(applications): show status indication [EE-1623] (#5614)
* feat(applications): show status indication

* feat(k8s/applications): move colors to theme

* fix helm application indicator for main header

* refactor(k8s/apps): receive more general ok status

Co-authored-by: waysonwei <degui.wei@gmail.com>
2021-09-29 10:10:51 +10:00
Matt Hook
7b72130433 feat(kubeshell) allow overriding default kubeshell image EE-1756 (#5755)
* feat(kubeshell) allow overriding default kubeshell

* Add missing error check and struct tag

* Add migrator for kube shell image and add it as a default in the db

* Fix file name to match migrator pattern

* remove default as it's now coming from the db

* remove blank line

* - conflict resolution code update
- logging migration error on migration failures

* - migrateDBVersionTo34 -> migrateDBVersionToDB34 (naming consistency)

Co-authored-by: zees-dev <dev.786zshan@gmail.com>
2021-09-29 11:39:45 +13:00
testA113
7611cc415a added selectors (#5616)
* added selectors

* moved selector to html element
2021-09-28 22:10:41 +13:00
Sven Dowideit
9045e17cba fix(docker): EE-348: fix Docker stats when using cgroups v2 (#5609)
Signed-off-by: Sven Dowideit <sven@mini.home.org.au>
2021-09-28 13:40:04 +10:00
Anthony Lapenna
46ffca92fd feat(k8s): remove cluster status panel (#5570) 2021-09-28 13:48:06 +13:00
Richard Wei
f0a88b7367 add wiggle room back to edge endpoint (#5739) 2021-09-27 20:33:46 +13:00
Sven Dowideit
7437006359 fix(swagger): EE-868: elide the password field in the swagger docs (#5636)
Signed-off-by: Sven Dowideit <SvenDowideit@home.org.au>
2021-09-27 14:00:04 +10:00
Sven Dowideit
9c80501738 fix(k8s): EE-1631: backport fixes for API proxy (#5608)
* fix(k8s): EE-1585: the K8s API uses other mediatypes, so we can't rely on parsing JSON bodies for security.

Signed-off-by: Sven Dowideit <SvenDowideit@home.org.au>

* fix(k8s): EE-1511 add striped prefix back to location header if response status is 301 moved permanently

Signed-off-by: Sven Dowideit <SvenDowideit@home.org.au>

* feat(k8s): EE-1631:improve the secrets handling by removing un-necessary code

Signed-off-by: Sven Dowideit <SvenDowideit@home.org.au>
2021-09-27 13:16:17 +10:00
zees-dev
377326085d feat(db): upgrade auto-backup backup and rollback support EE-867 EE-1158 (#5341)
* backport migration EE code structure

* filesystem copy function

* set db status to updating before migration - reset on completion

* support for auto-backup on version upgrade

* - rollback cli flag support (with confirmation)
- rollback implementation backport from EE

* removed edition as it is not required in CE

* migrated test datastore from bolttest to bolt package to make it usable for testing

* backported failsafe migration

* - backported tests from EE
- refactored tests to use test datastore

* test store implementing datastore interface

* addressed PR issues/improvements

* refactor test

* added backup file removal error logging

* resolved conflicts, updated code

* fixed missing bolttest package - migrated to bolt

* feat(migration): wrap migration errors to provide context for failure EE-1742 (#5711)

* feat(migrator): wrap errors to provide more context to failures EE-1742

* add overall failure back in. diff log file

* updated helm tests pointing to correct teststore

Co-authored-by: Matt Hook <hookenz@gmail.com>
2021-09-27 13:52:50 +13:00
Richard Wei
03d34076d8 fix error message not last long enough for user to copy error (#5642) 2021-09-27 10:09:23 +13:00
huib-portainer
09cf4c1bbe Update Bug_report.md
fix(link): Fixed the link referencing how to obtain the Portainer logs
2021-09-27 09:59:44 +13: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
Chaim Lev-Ari
db04bc9f38 fix(k8s/ns): validate ingress ctrl host pattern (#5663)
* fix(k8s/ns): validate ingress ctrl host pattern

* feat(kube/ns): validate ingress hostname
2021-09-24 14:02:06 +03:00
zees-dev
7d40a83d03 feat(kubectl-shell): page state refreshes in k8s endpoint do not close shell EE-1628 (#5685)
* converting all kubernetes view reload to partial state heirarchy refresh

* updated helm and kube kustom templates headers to use the reusable k8s page header component
2021-09-24 20:21:50 +12:00
Chaim Lev-Ari
d4f581a596 feat(kube): use local kubectl for all deployments (#5488) 2021-09-24 16:56:22 +12:00
testA113
5ad3cacefd Xt 321 automate k8s smoke test data cy attributes (#5734)
* added data-cy attributes for robust ui test automation
2021-09-24 13:00:55 +12:00
Richard Wei
6ac9c4367e show ip address of pod (#5613) 2021-09-23 14:34:24 +12:00
waysonwei
4bdf3ecf58 fix decl.moveTo is not a function error in css 2021-09-23 14:15:12 +12:00
Simon Meng
8aa03bb81b Merge remote-tracking branch 'origin/release/2.9' into develop
# Conflicts:
#	app/kubernetes/views/applications/create/createApplication.html
#	app/kubernetes/views/configurations/create/createConfiguration.html
2021-09-23 12:09:13 +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
fhanportainer
d14c7b0309 fix(name): fixed namespace creation issue when a registry attached. (#5646)
* fix(name): fixed namespace creation issue when a registry attached.

* fix(name): moved copy object to upper level of the function
2021-09-23 09:13:25 +12:00
fhanportainer
cbeb13636c fix(name): fixed namespace creation issue when a registry attached. (#5675) 2021-09-23 09:13:19 +12:00
Hui
a6138dd5a3 fix(migration): add debug logging for volume migration (#5700)
* add debug logging

* Update api/bolt/migrator/migrate_dbversion31.go

* log resource control delete

Co-authored-by: Stéphane Busso <sbusso@users.noreply.github.com>
2021-09-23 09:12:39 +12:00
Hui
5752e74be6 add debug log (#5702) 2021-09-23 09:12:35 +12:00
Matt Hook
cb37497444 doc(readme) fix slack link (#5701)
* doc(readme) fix slack link

* Use shared invite link underneath
2021-09-23 08:59:29 +12:00
Chaim Lev-Ari
0b64250647 Revert "refactor(settings): backport auth views (#5672)" (#5704)
This reverts commit 45af1f3d8b.
2021-09-22 10:17:22 +03:00
Chaim Lev-Ari
45af1f3d8b refactor(settings): backport auth views (#5672) 2021-09-22 10:11:53 +03:00
Chaim Lev-Ari
fc52830c7d fix(customtemplates): show correct type (#5669) 2021-09-22 08:00:16 +03:00
Chaim Lev-Ari
4890f50443 fix(customtemplates): show correct type (#5668) 2021-09-22 08:00:11 +03:00
Chaim Lev-Ari
6d510c4f30 fix(k8s/apps): edit url deployed app (#5652) 2021-09-22 07:59:32 +03:00
Chaim Lev-Ari
cad530ec04 fix(k8s/apps): edit url deployed app (#5653) 2021-09-22 07:59:28 +03:00
Chaim Lev-Ari
e63732484a fix(registries): put anon docker at top (#5671) 2021-09-22 07:55:28 +03:00
Chaim Lev-Ari
ec3233fb09 fix(registries): put anon docker at top (#5670) 2021-09-22 07:55:25 +03:00
Richard Wei
bcdc342cbd fix(k8s): fixerror handling for namespace restricted user EE-1703 (#5693)
* fix error handler for namespace when user have no namespace access
2021-09-22 16:01:42 +12:00
Richard Wei
e1f725d01a fix(k8s): fix error handling for namespace restricted user EE-1703 (#5692)
* fix error handler for user has no namespace access
2021-09-22 16:01:28 +12:00
Richard Wei
b876f2d17d fix danger button hover color (#5605) 2021-09-22 15:17:52 +12:00
mariyam-portainer
b0ec67826c Rename portainerbusiness.yml to config.yml 2021-09-22 15:07:23 +12:00
mariyam-portainer
b89d828878 Rename Portainer Business to portainerbusiness.yml 2021-09-22 15:06:25 +12:00
mariyam-portainer
e59df8134d Create Portainer Business 2021-09-22 15:04:05 +12:00
zees-dev
092d217985 table settings propagated through nested tables (#5699) 2021-09-22 13:42:13 +12:00
zees-dev
ad94162019 table settings propagated through nested tables (#5698) 2021-09-22 13:42:04 +12:00
Richard Wei
0efbf5bbf3 rename endpoint to environment in wizard breadcrumb header (#5696) 2021-09-22 13:18:52 +12:00
Richard Wei
c26ba23c53 rename endpoint to environment in wizard breadcrumb header (#5697) 2021-09-22 13:18:42 +12:00
Richard Wei
69096f664d fit(ui): use new portainer in login page and license page EE-1637 (#5604)
* use new portainer in login page and license page
2021-09-22 11:16:12 +12:00
Richard Wei
48c762c98b fix(notification): fix error in kube application stack name with space EE-1726 (#5691)
* fix error in kube application stack name with space
2021-09-21 20:58:23 +12:00
Richard Wei
488d86d200 fix(notification): fix error in kube application stack name with space EE-1726 (#5690)
* fix error in kube application stack name with space
2021-09-21 20:58:08 +12:00
Richard Wei
f10e0e4124 fix application table background not working with dark mode (#5617) 2021-09-21 19:29:25 +12:00
cong meng
5316cca3de fix(edge) EE-1733 cant edit edge groups (#5689)
* fix(edge) EE-1733 cant edit edge groups

* fix(edge) EE-1733 correct json names of a few edge objects

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-09-21 17:41:27 +12:00
cong meng
4267304e50 fix(edge) EE-1733 cant edit edge groups (#5687)
* fix(edge) EE-1733 cant edit edge groups

* fix(edge) EE-1733 correct json names of a few edge objects

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-09-21 17:41:14 +12:00
Richard Wei
deecbadce1 fix(k8s):fix difficulties selecting mixed protocols when creating k8s application EE-1073 (#5591)
* fix difficulties selecting mixed protocols when creating k8s application
2021-09-21 16:20:22 +12:00
fhanportainer
ecc9813750 fix(stack): fixed issue cannot deploy git stack without username. (#5680) 2021-09-21 13:42:04 +12:00
fhanportainer
24f11902b2 fix(stack): fixed issue cannot deploy git stack without username. (#5681) 2021-09-21 13:42:01 +12:00
cong meng
33118babdd fix(k8s) keep tunnel alive for websocket connection EE-1690 (#5677)
* fix(k8s) EE-1690 keep tunnel alive for websocket connection

* fix(k8s) EE-1690 fix comment

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-09-21 13:12:37 +12:00
cong meng
2aec348814 fix(k8s) keep tunnel alive for websocket connection EE-1690 (#5679)
* fix(k8s) EE-1690 keep tunnel alive for websocket connection

* fix(k8s) EE-1690 fix comment

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-09-21 13:12:31 +12:00
Richard Wei
4d63459d67 fix edge heartbeat show red when use search filter (#5682) 2021-09-21 10:19:22 +12:00
Richard Wei
483559af09 fix edge heartbeat turn red when use search filter (#5683) 2021-09-21 10:19:18 +12:00
Richard Wei
1796545d2e fix authentication toggle on by default - set to off (#5666) 2021-09-20 22:36:22 +12:00
Richard Wei
a50795063c fix git stack authentication on by default - set to off (#5667) 2021-09-20 22:36:16 +12:00
Richard Wei
7c9f7a2a8b fix error description on stats for non-admin users (#5665) 2021-09-20 15:41:46 +12:00
Richard Wei
af8065e8c2 fix error description on stats for non-admin user (#5664) 2021-09-20 15:41:40 +12:00
Richard Wei
dc769b4c4d fix icon not displayed when template created via upload file (#5658) 2021-09-20 12:20:38 +12:00
Richard Wei
dd808bb7bd fix(swagger): fix swagger api docs endpoint(s) rename to environment(s) EE-1661 (#5629)
* fix swagger api docs endpoint(s) rename to environment(s)
2021-09-20 12:14:22 +12:00
zees-dev
d911c50f1b fixed k8s app edit config dropdown default (#5651) 2021-09-20 11:08:18 +12:00
zees-dev
f6f31b8872 fixed docker image pull text on error scenario (#5656) 2021-09-20 01:42:55 +12:00
Chaim Lev-Ari
1f4a7b32e3 fix(customtemplate): edit custom template [EE-1691] (#5633) 2021-09-17 09:24:23 +03:00
zees-dev
a781021072 docker image pull toast fix (#5644) 2021-09-17 18:22:57 +12:00
Matt Hook
9492e30dc2 feat(helm/tests): update libhelm with new search mock EE-1599 (#5615)
* feat(helm/tests) add repo search and update libhelm with new mock EE-1599

* also enable repo search test
2021-09-16 16:56:46 +12:00
zees-dev
d2cbdf935a using new app metadata property to distinguish helm apps (#5627) 2021-09-16 16:09:39 +12:00
zees-dev
05efac44f6 helm templates blog post link fix (#5626) 2021-09-16 10:00:55 +12:00
zees-dev
555c9f238f fix webpack dev server (#5631) 2021-09-15 17:55:06 +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
432 changed files with 11509 additions and 4945 deletions

View File

@@ -30,7 +30,7 @@ A clear and concise description of what you expected to happen.
**Portainer Logs**
Provide the logs of your Portainer container or Service.
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
You can see how [here](https://documentation.portainer.io/r/portainer-logs)
**Steps to reproduce the issue:**

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Portainer Business
url: https://www.portainer.io/portainerbusiness
about: Would you and your co-workers benefit from our enterprise edition which provides functionality to deploy Portainer at scale?

View File

@@ -44,7 +44,7 @@ Portainer CE is an open source project and is supported by the community. You ca
Learn more about Portainers community support channels [here.](https://www.portainer.io/help_about)
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): https://portainer.io/slack/
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
You can join the Portainer Community by visiting community.portainer.io. This will give you advance notice of events, content and other related Portainer content.
@@ -59,7 +59,7 @@ You can join the Portainer Community by visiting community.portainer.io. This wi
## WORK FOR US
If you are a developer, and our code in this repo makes sense to you, we would love to hear from you. We are always on the hunt for awesome devs, either freelance or employed. Drop us a line to info@portainer.io with your details and we will be in touch.
If you are a developer, and our code in this repo makes sense to you, we would love to hear from you. We are always on the hunt for awesome devs, either freelance or employed. Drop us a line to info@portainer.io with your details and we will be in touch.
## Privacy

142
api/bolt/backup.go Normal file
View File

@@ -0,0 +1,142 @@
package bolt
import (
"fmt"
"os"
"path"
"time"
plog "github.com/portainer/portainer/api/bolt/log"
)
var backupDefaults = struct {
backupDir string
commonDir string
databaseFileName string
}{
"backups",
"common",
databaseFileName,
}
var backupLog = plog.NewScopedLog("bolt, backup")
//
// Backup Helpers
//
// createBackupFolders create initial folders for backups
func (store *Store) createBackupFolders() {
// create common dir
commonDir := store.commonBackupDir()
if exists, _ := store.fileService.FileExists(commonDir); !exists {
if err := os.MkdirAll(commonDir, 0700); err != nil {
backupLog.Error("Error while creating common backup folder", err)
}
}
}
func (store *Store) databasePath() string {
return path.Join(store.path, databaseFileName)
}
func (store *Store) commonBackupDir() string {
return path.Join(store.path, backupDefaults.backupDir, backupDefaults.commonDir)
}
func (store *Store) copyDBFile(from string, to string) error {
backupLog.Info(fmt.Sprintf("Copying db file from %s to %s", from, to))
err := store.fileService.Copy(from, to, true)
if err != nil {
backupLog.Error("Failed", err)
}
return err
}
// BackupOptions provide a helper to inject backup options
type BackupOptions struct {
Version int
BackupDir string
BackupFileName string
BackupPath string
}
func (store *Store) setupOptions(options *BackupOptions) *BackupOptions {
if options == nil {
options = &BackupOptions{}
}
if options.Version == 0 {
options.Version, _ = store.version()
}
if options.BackupDir == "" {
options.BackupDir = store.commonBackupDir()
}
if options.BackupFileName == "" {
options.BackupFileName = fmt.Sprintf("%s.%s.%s", backupDefaults.databaseFileName, fmt.Sprintf("%03d", options.Version), time.Now().Format("20060102150405"))
}
if options.BackupPath == "" {
options.BackupPath = path.Join(options.BackupDir, options.BackupFileName)
}
return options
}
// BackupWithOptions backup current database with options
func (store *Store) BackupWithOptions(options *BackupOptions) (string, error) {
backupLog.Info("creating db backup")
store.createBackupFolders()
options = store.setupOptions(options)
return options.BackupPath, store.copyDBFile(store.databasePath(), options.BackupPath)
}
// RestoreWithOptions previously saved backup for the current Edition with options
// Restore strategies:
// - default: restore latest from current edition
// - restore a specific
func (store *Store) RestoreWithOptions(options *BackupOptions) error {
options = store.setupOptions(options)
// Check if backup file exist before restoring
_, err := os.Stat(options.BackupPath)
if os.IsNotExist(err) {
backupLog.Error(fmt.Sprintf("Backup file to restore does not exist %s", options.BackupPath), err)
return err
}
err = store.Close()
if err != nil {
backupLog.Error("Error while closing store before restore", err)
return err
}
backupLog.Info("Restoring db backup")
err = store.copyDBFile(options.BackupPath, store.databasePath())
if err != nil {
return err
}
return store.Open()
}
// RemoveWithOptions removes backup database based on supplied options
func (store *Store) RemoveWithOptions(options *BackupOptions) error {
backupLog.Info("Removing db backup")
options = store.setupOptions(options)
_, err := os.Stat(options.BackupPath)
if os.IsNotExist(err) {
backupLog.Error(fmt.Sprintf("Backup file to remove does not exist %s", options.BackupPath), err)
return err
}
backupLog.Info(fmt.Sprintf("Removing db file at %s", options.BackupPath))
err = os.Remove(options.BackupPath)
if err != nil {
backupLog.Error("Failed", err)
return err
}
return nil
}

116
api/bolt/backup_test.go Normal file
View File

@@ -0,0 +1,116 @@
package bolt
import (
"fmt"
"os"
"path"
"path/filepath"
"testing"
portainer "github.com/portainer/portainer/api"
)
// isFileExist is helper function to check for file existence
func isFileExist(path string) bool {
matches, err := filepath.Glob(path)
if err != nil {
return false
}
return len(matches) > 0
}
func TestCreateBackupFolders(t *testing.T) {
store, teardown := MustNewTestStore(false)
defer teardown()
backupPath := path.Join(store.path, backupDefaults.backupDir)
if isFileExist(backupPath) {
t.Error("Expect backups folder to not exist")
}
store.createBackupFolders()
if !isFileExist(backupPath) {
t.Error("Expect backups folder to exist")
}
}
func TestStoreCreation(t *testing.T) {
store, teardown := MustNewTestStore(true)
defer teardown()
if store == nil {
t.Error("Expect to create a store")
}
if store.edition() != portainer.PortainerCE {
t.Error("Expect to get CE Edition")
}
}
func TestBackup(t *testing.T) {
store, teardown := MustNewTestStore(true)
defer teardown()
t.Run("Backup should create default db backup", func(t *testing.T) {
store.VersionService.StoreDBVersion(portainer.DBVersion)
store.BackupWithOptions(nil)
backupFileName := path.Join(store.path, "backups", "common", fmt.Sprintf("portainer.db.%03d.*", portainer.DBVersion))
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}
})
t.Run("BackupWithOption should create a name specific backup at common path", func(t *testing.T) {
store.BackupWithOptions(&BackupOptions{
BackupFileName: beforePortainerVersionUpgradeBackup,
BackupDir: store.commonBackupDir(),
})
backupFileName := path.Join(store.path, "backups", "common", beforePortainerVersionUpgradeBackup)
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
}
})
}
func TestRemoveWithOptions(t *testing.T) {
store, teardown := MustNewTestStore(true)
defer teardown()
t.Run("successfully removes file if existent", func(t *testing.T) {
store.createBackupFolders()
options := &BackupOptions{
BackupDir: store.commonBackupDir(),
BackupFileName: "test.txt",
}
filePath := path.Join(options.BackupDir, options.BackupFileName)
f, err := os.Create(filePath)
if err != nil {
t.Fatalf("file should be created; err=%s", err)
}
f.Close()
err = store.RemoveWithOptions(options)
if err != nil {
t.Errorf("RemoveWithOptions should successfully remove file; err=%w", err)
}
if isFileExist(f.Name()) {
t.Errorf("RemoveWithOptions should successfully remove file; file=%s", f.Name())
}
})
t.Run("fails to removes file if non-existent", func(t *testing.T) {
options := &BackupOptions{
BackupDir: store.commonBackupDir(),
BackupFileName: "test.txt",
}
err := store.RemoveWithOptions(options)
if err == nil {
t.Error("RemoveWithOptions should fail for non-existent file")
}
})
}

View File

@@ -2,7 +2,6 @@ package bolt
import (
"io"
"log"
"path"
"time"
@@ -21,7 +20,6 @@ import (
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/extension"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/portainer/portainer/api/bolt/migrator"
"github.com/portainer/portainer/api/bolt/registry"
"github.com/portainer/portainer/api/bolt/resourcecontrol"
"github.com/portainer/portainer/api/bolt/role"
@@ -36,7 +34,6 @@ import (
"github.com/portainer/portainer/api/bolt/user"
"github.com/portainer/portainer/api/bolt/version"
"github.com/portainer/portainer/api/bolt/webhook"
"github.com/portainer/portainer/api/internal/authorization"
)
const (
@@ -76,6 +73,14 @@ type Store struct {
WebhookService *webhook.Service
}
func (store *Store) version() (int, error) {
version, err := store.VersionService.DBVersion()
if err == errors.ErrObjectNotFound {
version = 0
}
return version, err
}
func (store *Store) edition() portainer.SoftwareEdition {
edition, err := store.VersionService.Edition()
if err == errors.ErrObjectNotFound {
@@ -85,25 +90,13 @@ func (store *Store) edition() portainer.SoftwareEdition {
}
// NewStore initializes a new Store and the associated services
func NewStore(storePath string, fileService portainer.FileService) (*Store, error) {
store := &Store{
func NewStore(storePath string, fileService portainer.FileService) *Store {
return &Store{
path: storePath,
fileService: fileService,
isNew: true,
connection: &internal.DbConnection{},
}
databasePath := path.Join(storePath, databaseFileName)
databaseFileExists, err := fileService.FileExists(databasePath)
if err != nil {
return nil, err
}
if databaseFileExists {
store.isNew = false
}
return store, nil
}
// Open opens and initializes the BoltDB database.
@@ -115,7 +108,17 @@ func (store *Store) Open() error {
}
store.connection.DB = db
return store.initServices()
err = store.initServices()
if err != nil {
return err
}
// if we have DBVersion in the database then ensure we flag this as NOT a new store
if _, err := store.VersionService.DBVersion(); err == nil {
store.isNew = false
}
return nil
}
// Close closes the BoltDB database.
@@ -133,64 +136,6 @@ func (store *Store) IsNew() bool {
return store.isNew
}
// CheckCurrentEdition checks if current edition is community edition
func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.PortainerCE {
return errors.ErrWrongDBEdition
}
return nil
}
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
// if force is true, then migrate regardless.
func (store *Store) MigrateData(force bool) error {
if store.isNew && !force {
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
version, err := store.VersionService.DBVersion()
if err == errors.ErrObjectNotFound {
version = 0
} else if err != nil {
return err
}
if version < portainer.DBVersion {
migratorParams := &migrator.Parameters{
DB: store.connection.DB,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store),
}
migrator := migrator.NewMigrator(migratorParams)
log.Printf("Migrating database from version %v to %v.\n", version, portainer.DBVersion)
err = migrator.Migrate()
if err != nil {
log.Printf("An error occurred during database migration: %s\n", err)
return err
}
}
return nil
}
// BackupTo backs up db to a provided writer.
// It does hot backup and doesn't block other database reads and writes
func (store *Store) BackupTo(w io.Writer) error {
@@ -199,3 +144,11 @@ func (store *Store) BackupTo(w io.Writer) error {
return err
})
}
// CheckCurrentEdition checks if current edition is community edition
func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.PortainerCE {
return errors.ErrWrongDBEdition
}
return nil
}

View File

@@ -47,6 +47,7 @@ func (store *Store) Init() error {
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
KubectlShellImage: portainer.DefaultKubectlShellImage,
}
err = store.SettingsService.UpdateSettings(defaultSettings)

146
api/bolt/migrate_data.go Normal file
View File

@@ -0,0 +1,146 @@
package bolt
import (
"fmt"
"github.com/portainer/portainer/api/cli"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
plog "github.com/portainer/portainer/api/bolt/log"
"github.com/portainer/portainer/api/bolt/migrator"
"github.com/portainer/portainer/api/internal/authorization"
)
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 {
defer func() {
if err := recover(); err != nil {
migrateLog.Info(fmt.Sprintf("Error during migration, recovering [%v]", err))
store.Rollback(true)
}
}()
return migrator.Migrate()
}
// MigrateData automatically migrate the data based on the DBVersion.
// This process is only triggered on an existing database, not if the database was just created.
// if force is true, then migrate regardless.
func (store *Store) MigrateData(force bool) error {
if store.isNew && !force {
return store.VersionService.StoreDBVersion(portainer.DBVersion)
}
migrator, err := store.newMigrator()
if err != nil {
return err
}
// backup db file before upgrading DB to support rollback
isUpdating, err := store.VersionService.IsUpdating()
if err != nil && err != errors.ErrObjectNotFound {
return err
}
if !isUpdating && migrator.Version() != portainer.DBVersion {
err = store.backupVersion(migrator)
if err != nil {
return werrors.Wrapf(err, "failed to backup database")
}
}
if migrator.Version() < portainer.DBVersion {
migrateLog.Info(fmt.Sprintf("Migrating database from version %v to %v.\n", migrator.Version(), portainer.DBVersion))
err = store.FailSafeMigrate(migrator)
if err != nil {
migrateLog.Error("An error occurred during database migration", err)
return err
}
}
return nil
}
func (store *Store) newMigrator() (*migrator.Migrator, error) {
version, err := store.version()
if err != nil {
return nil, err
}
migratorParams := &migrator.Parameters{
DB: store.connection.DB,
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
EndpointRelationService: store.EndpointRelationService,
ExtensionService: store.ExtensionService,
RegistryService: store.RegistryService,
ResourceControlService: store.ResourceControlService,
RoleService: store.RoleService,
ScheduleService: store.ScheduleService,
SettingsService: store.SettingsService,
StackService: store.StackService,
TagService: store.TagService,
TeamMembershipService: store.TeamMembershipService,
UserService: store.UserService,
VersionService: store.VersionService,
FileService: store.fileService,
DockerhubService: store.DockerHubService,
AuthorizationService: authorization.NewService(store),
}
return migrator.NewMigrator(migratorParams), nil
}
// getBackupRestoreOptions returns options to store db at common backup dir location; used by:
// - db backup prior to version upgrade
// - db rollback
func getBackupRestoreOptions(store *Store) *BackupOptions {
return &BackupOptions{
BackupDir: store.commonBackupDir(),
BackupFileName: beforePortainerVersionUpgradeBackup,
}
}
// backupVersion will backup the database or panic if any errors occur
func (store *Store) backupVersion(migrator *migrator.Migrator) error {
migrateLog.Info("Backing up database prior to version upgrade...")
options := getBackupRestoreOptions(store)
_, err := store.BackupWithOptions(options)
if err != nil {
migrateLog.Error("An error occurred during database backup", err)
removalErr := store.RemoveWithOptions(options)
if removalErr != nil {
migrateLog.Error("An error occurred during store removal prior to backup", err)
}
return err
}
return nil
}
// Rollback to a pre-upgrade backup copy/snapshot of portainer.db
func (store *Store) Rollback(force bool) error {
if !force {
confirmed, err := cli.Confirm("Are you sure you want to rollback your database to the previous backup?")
if err != nil || !confirmed {
return err
}
}
options := getBackupRestoreOptions(store)
err := store.RestoreWithOptions(options)
if err != nil {
return err
}
return store.Close()
}

View File

@@ -0,0 +1,172 @@
package bolt
import (
"fmt"
"log"
"strings"
"testing"
portainer "github.com/portainer/portainer/api"
)
// testVersion is a helper which tests current store version against wanted version
func testVersion(store *Store, versionWant int, t *testing.T) {
if v, _ := store.version(); v != versionWant {
t.Errorf("Expect store version to be %d but was %d", versionWant, v)
}
}
func TestMigrateData(t *testing.T) {
t.Run("MigrateData for New Store & Re-Open Check", func(t *testing.T) {
store, teardown := MustNewTestStore(false)
defer teardown()
if !store.IsNew() {
t.Error("Expect a new DB")
}
store.MigrateData(false)
testVersion(store, portainer.DBVersion, t)
store.Close()
store.Open()
if store.IsNew() {
t.Error("Expect store to NOT be new DB")
}
})
tests := []struct {
version int
expectedVersion int
}{
{version: 2, expectedVersion: portainer.DBVersion},
{version: 21, expectedVersion: portainer.DBVersion},
}
for _, tc := range tests {
store, teardown := MustNewTestStore(true)
defer teardown()
// Setup data
store.VersionService.StoreDBVersion(tc.version)
// Required roles by migrations 22.2
store.RoleService.CreateRole(&portainer.Role{ID: 1})
store.RoleService.CreateRole(&portainer.Role{ID: 2})
store.RoleService.CreateRole(&portainer.Role{ID: 3})
store.RoleService.CreateRole(&portainer.Role{ID: 4})
t.Run(fmt.Sprintf("MigrateData for version %d", tc.version), func(t *testing.T) {
store.MigrateData(true)
testVersion(store, tc.expectedVersion, t)
})
t.Run(fmt.Sprintf("Restoring DB after migrateData for version %d", tc.version), func(t *testing.T) {
store.Rollback(true)
store.Open()
testVersion(store, tc.version, t)
})
}
t.Run("Error in MigrateData should restore backup before MigrateData", func(t *testing.T) {
store, teardown := MustNewTestStore(false)
defer teardown()
version := 2
store.VersionService.StoreDBVersion(version)
store.MigrateData(true)
testVersion(store, version, t)
})
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreDBVersion(0)
store.MigrateData(true)
options := store.setupOptions(getBackupRestoreOptions(store))
if !isFileExist(options.BackupPath) {
t.Errorf("Backup file should exist; file=%s", options.BackupPath)
}
})
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreIsUpdating(true)
store.MigrateData(true)
options := store.setupOptions(getBackupRestoreOptions(store))
if isFileExist(options.BackupPath) {
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
}
})
t.Run("MigrateData should not create backup on startup if portainer version matches db", func(t *testing.T) {
store, teardown := MustNewTestStore(false)
defer teardown()
store.MigrateData(true)
options := store.setupOptions(getBackupRestoreOptions(store))
if isFileExist(options.BackupPath) {
t.Errorf("Backup file should not exist for dirty database; file=%s", options.BackupPath)
}
})
}
func Test_getBackupRestoreOptions(t *testing.T) {
store, teardown := MustNewTestStore(false)
defer teardown()
options := getBackupRestoreOptions(store)
wantDir := store.commonBackupDir()
if !strings.HasSuffix(options.BackupDir, wantDir) {
log.Fatalf("incorrect backup dir; got=%s, want=%s", options.BackupDir, wantDir)
}
wantFilename := "portainer.db.bak"
if options.BackupFileName != wantFilename {
log.Fatalf("incorrect backup file; got=%s, want=%s", options.BackupFileName, wantFilename)
}
}
func TestRollback(t *testing.T) {
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
version := 21
store, teardown := MustNewTestStore(false)
defer teardown()
store.VersionService.StoreDBVersion(version)
_, err := store.BackupWithOptions(getBackupRestoreOptions(store))
if err != nil {
log.Fatal(err)
}
// Change the current edition
err = store.VersionService.StoreDBVersion(version + 10)
if err != nil {
log.Fatal(err)
}
err = store.Rollback(true)
if err != nil {
t.Logf("Rollback failed: %s", err)
t.Fail()
return
}
store.Open()
testVersion(store, version, t)
})
}

View File

@@ -0,0 +1,327 @@
package migrator
import (
"fmt"
werrors "github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
func migrationError(err error, context string) error {
return werrors.Wrap(err, "failed in "+context)
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// set DB to updating status
err := m.versionService.StoreIsUpdating(true)
if err != nil {
return migrationError(err, "StoreIsUpdating")
}
// Portainer < 1.12
if m.currentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return migrationError(err, "updateAdminUserToDBVersion1")
}
}
// Portainer 1.12.x
if m.currentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return migrationError(err, "updateResourceControlsToDBVersion2")
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return migrationError(err, "updateEndpointsToDBVersion2")
}
}
// Portainer 1.13.x
if m.currentDBVersion < 3 {
err := m.updateSettingsToDBVersion3()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion3")
}
}
// Portainer 1.14.0
if m.currentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil {
return migrationError(err, "updateEndpointsToDBVersion4")
}
}
// https://github.com/portainer/portainer/issues/1235
if m.currentDBVersion < 5 {
err := m.updateSettingsToVersion5()
if err != nil {
return migrationError(err, "updateSettingsToVersion5")
}
}
// https://github.com/portainer/portainer/issues/1236
if m.currentDBVersion < 6 {
err := m.updateSettingsToVersion6()
if err != nil {
return migrationError(err, "updateSettingsToVersion6")
}
}
// https://github.com/portainer/portainer/issues/1449
if m.currentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return migrationError(err, "updateSettingsToVersion7")
}
}
if m.currentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return migrationError(err, "updateEndpointsToVersion8")
}
}
// https: //github.com/portainer/portainer/issues/1396
if m.currentDBVersion < 9 {
err := m.updateEndpointsToVersion9()
if err != nil {
return migrationError(err, "updateEndpointsToVersion9")
}
}
// https://github.com/portainer/portainer/issues/461
if m.currentDBVersion < 10 {
err := m.updateEndpointsToVersion10()
if err != nil {
return migrationError(err, "updateEndpointsToVersion10")
}
}
// https://github.com/portainer/portainer/issues/1906
if m.currentDBVersion < 11 {
err := m.updateEndpointsToVersion11()
if err != nil {
return migrationError(err, "updateEndpointsToVersion11")
}
}
// Portainer 1.18.0
if m.currentDBVersion < 12 {
err := m.updateEndpointsToVersion12()
if err != nil {
return migrationError(err, "updateEndpointsToVersion12")
}
err = m.updateEndpointGroupsToVersion12()
if err != nil {
return migrationError(err, "updateEndpointGroupsToVersion12")
}
err = m.updateStacksToVersion12()
if err != nil {
return migrationError(err, "updateStacksToVersion12")
}
}
// Portainer 1.19.0
if m.currentDBVersion < 13 {
err := m.updateSettingsToVersion13()
if err != nil {
return migrationError(err, "updateSettingsToVersion13")
}
}
// Portainer 1.19.2
if m.currentDBVersion < 14 {
err := m.updateResourceControlsToDBVersion14()
if err != nil {
return migrationError(err, "updateResourceControlsToDBVersion14")
}
}
// Portainer 1.20.0
if m.currentDBVersion < 15 {
err := m.updateSettingsToDBVersion15()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion15")
}
err = m.updateTemplatesToVersion15()
if err != nil {
return migrationError(err, "updateTemplatesToVersion15")
}
}
if m.currentDBVersion < 16 {
err := m.updateSettingsToDBVersion16()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion16")
}
}
// Portainer 1.20.1
if m.currentDBVersion < 17 {
err := m.updateExtensionsToDBVersion17()
if err != nil {
return migrationError(err, "updateExtensionsToDBVersion17")
}
}
// Portainer 1.21.0
if m.currentDBVersion < 18 {
err := m.updateUsersToDBVersion18()
if err != nil {
return migrationError(err, "updateUsersToDBVersion18")
}
err = m.updateEndpointsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointsToDBVersion18")
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return migrationError(err, "updateEndpointGroupsToDBVersion18")
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return migrationError(err, "updateRegistriesToDBVersion18")
}
}
// Portainer 1.22.0
if m.currentDBVersion < 19 {
err := m.updateSettingsToDBVersion19()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion19")
}
}
// Portainer 1.22.1
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return migrationError(err, "updateUsersToDBVersion20")
}
err = m.updateSettingsToDBVersion20()
if err != nil {
return migrationError(err, "updateSettingsToDBVersion20")
}
err = m.updateSchedulesToDBVersion20()
if err != nil {
return migrationError(err, "updateSchedulesToDBVersion20")
}
}
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
if m.currentDBVersion < 22 {
err := m.updateResourceControlsToDBVersion22()
if err != nil {
return migrationError(err, "updateResourceControlsToDBVersion22")
}
err = m.updateUsersAndRolesToDBVersion22()
if err != nil {
return migrationError(err, "updateUsersAndRolesToDBVersion22")
}
}
// Portainer 1.24.0
if m.currentDBVersion < 23 {
err := m.updateTagsToDBVersion23()
if err != nil {
return migrationError(err, "updateTagsToDBVersion23")
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return migrationError(err, "updateEndpointsAndEndpointGroupsToDBVersion23")
}
}
// Portainer 1.24.1
if m.currentDBVersion < 24 {
err := m.updateSettingsToDB24()
if err != nil {
return migrationError(err, "updateSettingsToDB24")
}
}
// Portainer 2.0.0
if m.currentDBVersion < 25 {
err := m.updateSettingsToDB25()
if err != nil {
return migrationError(err, "updateSettingsToDB25")
}
err = m.updateStacksToDB24()
if err != nil {
return migrationError(err, "updateStacksToDB24")
}
}
// Portainer 2.1.0
if m.currentDBVersion < 26 {
err := m.updateEndpointSettingsToDB25()
if err != nil {
return migrationError(err, "updateEndpointSettingsToDB25")
}
}
// Portainer 2.2.0
if m.currentDBVersion < 27 {
err := m.updateStackResourceControlToDB27()
if err != nil {
return migrationError(err, "updateStackResourceControlToDB27")
}
}
// Portainer 2.6.0
if m.currentDBVersion < 30 {
err := m.migrateDBVersionToDB30()
if err != nil {
return migrationError(err, "migrateDBVersionToDB30")
}
}
// Portainer 2.9.0
if m.currentDBVersion < 32 {
err := m.migrateDBVersionToDB32()
if err != nil {
return migrationError(err, "migrateDBVersionToDB32")
}
}
// Portainer 2.9.1
if m.currentDBVersion < 33 {
err := m.migrateDBVersionToDB33()
if err != nil {
return migrationError(err, "migrateDBVersionToDB33")
}
}
// Portainer 2.10
if m.currentDBVersion < 34 {
if err := m.migrateDBVersionToDB34(); err != nil {
return migrationError(err, "migrateDBVersionToDB34")
}
}
err = m.versionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return migrationError(err, "StoreDBVersion")
}
migrateLog.Info(fmt.Sprintf("Updated DB version to %d", portainer.DBVersion))
// reset DB updating status
return m.versionService.StoreIsUpdating(false)
}

View File

@@ -2,6 +2,7 @@ package migrator
import (
"fmt"
"log"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
@@ -167,6 +168,7 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
totalSnapshots := len(endpoint.Snapshots)
if totalSnapshots == 0 {
log.Println("[DEBUG] [volume migration] [message: no snapshot found]")
continue
}
@@ -174,11 +176,13 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
endpointDockerID, err := snapshotutils.FetchDockerID(snapshot)
if err != nil {
return fmt.Errorf("failed fetching environment docker id: %w", err)
log.Printf("[WARN] [bolt,migrator,v31] [message: failed fetching environment docker id] [err: %s]", err)
continue
}
if volumesData, done := snapshot.SnapshotRaw.Volumes.(map[string]interface{}); done {
if volumesData["Volumes"] == nil {
log.Println("[DEBUG] [volume migration] [message: no volume data found]")
continue
}
@@ -199,7 +203,7 @@ func (m *Migrator) updateVolumeResourceControlToDB32() error {
if err != nil {
return fmt.Errorf("failed deleting resource control %d: %w", resourceControl.ID, err)
}
log.Printf("[DEBUG] [volume migration] [message: legacy resource control(%s) has been deleted]", resourceControl.ResourceID)
}
}
@@ -210,7 +214,11 @@ func findResourcesToUpdateForDB32(dockerID string, volumesData map[string]interf
volumes := volumesData["Volumes"].([]interface{})
for _, volumeMeta := range volumes {
volume := volumeMeta.(map[string]interface{})
volumeName := volume["Name"].(string)
volumeName, nameExist := volume["Name"].(string)
if !nameExist {
continue
}
oldResourceID := fmt.Sprintf("%s%s", volumeName, volume["CreatedAt"].(string))
resourceControl, ok := volumeResourceControls[oldResourceID]

View File

@@ -0,0 +1,21 @@
package migrator
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) migrateDBVersionToDB33() error {
if err := m.migrateSettingsToDB33(); err != nil {
return err
}
return nil
}
func (m *Migrator) migrateSettingsToDB33() error {
settings, err := m.settingsService.Settings()
if err != nil {
return err
}
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
return m.settingsService.UpdateSettings(settings)
}

View File

@@ -4,7 +4,7 @@ import (
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) migrateDBVersionTo33() error {
func (m *Migrator) migrateDBVersionToDB34() error {
err := migrateStackEntryPoint(m.stackService)
if err != nil {
return err

View File

@@ -14,7 +14,7 @@ import (
)
func TestMigrateStackEntryPoint(t *testing.T) {
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-33.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
dbConn, err := bolt.Open(path.Join(t.TempDir(), "portainer-ee-mig-34.db"), 0600, &bolt.Options{Timeout: 1 * time.Second})
assert.NoError(t, err, "failed to init testing DB connection")
defer dbConn.Close()

View File

@@ -27,8 +27,9 @@ var migrateLog = plog.NewScopedLog("bolt, migrate")
type (
// Migrator defines a service to migrate data after a Portainer version update.
Migrator struct {
currentDBVersion int
db *bolt.DB
db *bolt.DB
currentDBVersion int
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
@@ -97,295 +98,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
}
}
// Migrate checks the database version and migrate the existing data to the most recent data model.
func (m *Migrator) Migrate() error {
// Portainer < 1.12
if m.currentDBVersion < 1 {
err := m.updateAdminUserToDBVersion1()
if err != nil {
return err
}
}
// Portainer 1.12.x
if m.currentDBVersion < 2 {
err := m.updateResourceControlsToDBVersion2()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion2()
if err != nil {
return err
}
}
// Portainer 1.13.x
if m.currentDBVersion < 3 {
err := m.updateSettingsToDBVersion3()
if err != nil {
return err
}
}
// Portainer 1.14.0
if m.currentDBVersion < 4 {
err := m.updateEndpointsToDBVersion4()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1235
if m.currentDBVersion < 5 {
err := m.updateSettingsToVersion5()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1236
if m.currentDBVersion < 6 {
err := m.updateSettingsToVersion6()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1449
if m.currentDBVersion < 7 {
err := m.updateSettingsToVersion7()
if err != nil {
return err
}
}
if m.currentDBVersion < 8 {
err := m.updateEndpointsToVersion8()
if err != nil {
return err
}
}
// https: //github.com/portainer/portainer/issues/1396
if m.currentDBVersion < 9 {
err := m.updateEndpointsToVersion9()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/461
if m.currentDBVersion < 10 {
err := m.updateEndpointsToVersion10()
if err != nil {
return err
}
}
// https://github.com/portainer/portainer/issues/1906
if m.currentDBVersion < 11 {
err := m.updateEndpointsToVersion11()
if err != nil {
return err
}
}
// Portainer 1.18.0
if m.currentDBVersion < 12 {
err := m.updateEndpointsToVersion12()
if err != nil {
return err
}
err = m.updateEndpointGroupsToVersion12()
if err != nil {
return err
}
err = m.updateStacksToVersion12()
if err != nil {
return err
}
}
// Portainer 1.19.0
if m.currentDBVersion < 13 {
err := m.updateSettingsToVersion13()
if err != nil {
return err
}
}
// Portainer 1.19.2
if m.currentDBVersion < 14 {
err := m.updateResourceControlsToDBVersion14()
if err != nil {
return err
}
}
// Portainer 1.20.0
if m.currentDBVersion < 15 {
err := m.updateSettingsToDBVersion15()
if err != nil {
return err
}
err = m.updateTemplatesToVersion15()
if err != nil {
return err
}
}
if m.currentDBVersion < 16 {
err := m.updateSettingsToDBVersion16()
if err != nil {
return err
}
}
// Portainer 1.20.1
if m.currentDBVersion < 17 {
err := m.updateExtensionsToDBVersion17()
if err != nil {
return err
}
}
// Portainer 1.21.0
if m.currentDBVersion < 18 {
err := m.updateUsersToDBVersion18()
if err != nil {
return err
}
err = m.updateEndpointsToDBVersion18()
if err != nil {
return err
}
err = m.updateEndpointGroupsToDBVersion18()
if err != nil {
return err
}
err = m.updateRegistriesToDBVersion18()
if err != nil {
return err
}
}
// Portainer 1.22.0
if m.currentDBVersion < 19 {
err := m.updateSettingsToDBVersion19()
if err != nil {
return err
}
}
// Portainer 1.22.1
if m.currentDBVersion < 20 {
err := m.updateUsersToDBVersion20()
if err != nil {
return err
}
err = m.updateSettingsToDBVersion20()
if err != nil {
return err
}
err = m.updateSchedulesToDBVersion20()
if err != nil {
return err
}
}
// Portainer 1.23.0
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
if m.currentDBVersion < 22 {
err := m.updateResourceControlsToDBVersion22()
if err != nil {
return err
}
err = m.updateUsersAndRolesToDBVersion22()
if err != nil {
return err
}
}
// Portainer 1.24.0
if m.currentDBVersion < 23 {
err := m.updateTagsToDBVersion23()
if err != nil {
return err
}
err = m.updateEndpointsAndEndpointGroupsToDBVersion23()
if err != nil {
return err
}
}
// Portainer 1.24.1
if m.currentDBVersion < 24 {
err := m.updateSettingsToDB24()
if err != nil {
return err
}
}
// Portainer 2.0.0
if m.currentDBVersion < 25 {
err := m.updateSettingsToDB25()
if err != nil {
return err
}
err = m.updateStacksToDB24()
if err != nil {
return err
}
}
// Portainer 2.1.0
if m.currentDBVersion < 26 {
err := m.updateEndpointSettingsToDB25()
if err != nil {
return err
}
}
// Portainer 2.2.0
if m.currentDBVersion < 27 {
err := m.updateStackResourceControlToDB27()
if err != nil {
return err
}
}
// Portainer 2.6.0
if m.currentDBVersion < 30 {
err := m.migrateDBVersionToDB30()
if err != nil {
return err
}
}
// Portainer 2.9.0
if m.currentDBVersion < 32 {
err := m.migrateDBVersionToDB32()
if err != nil {
return err
}
}
if m.currentDBVersion < 33 {
if err := m.migrateDBVersionTo33(); err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
// Version exposes version of database
func (migrator *Migrator) Version() int {
return migrator.currentDBVersion
}

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

@@ -4,18 +4,12 @@ import (
"testing"
"time"
"github.com/portainer/portainer/api/bolt"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/bolttest"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/filesystem"
"github.com/stretchr/testify/assert"
)
func newGuidString(t *testing.T) string {
@@ -35,7 +29,7 @@ func TestService_StackByWebhookID(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
store, teardown := bolttest.MustNewTestStore(true)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
b := stackBuilder{t: t, store: store}
@@ -93,7 +87,7 @@ func Test_RefreshableStacks(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
store, teardown := bolttest.MustNewTestStore(true)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
staticStack := portainer.Stack{ID: 1}

View File

@@ -1,4 +1,4 @@
package bolttest
package bolt
import (
"io/ioutil"
@@ -6,13 +6,12 @@ import (
"os"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/filesystem"
)
var errTempDir = errors.New("can't create a temp dir")
func MustNewTestStore(init bool) (*bolt.Store, func()) {
func MustNewTestStore(init bool) (*Store, func()) {
store, teardown, err := NewTestStore(init)
if err != nil {
if !errors.Is(err, errTempDir) {
@@ -24,7 +23,7 @@ func MustNewTestStore(init bool) (*bolt.Store, func()) {
return store, teardown
}
func NewTestStore(init bool) (*bolt.Store, func(), error) {
func NewTestStore(init bool) (*Store, func(), error) {
// Creates unique temp directory in a concurrency friendly manner.
dataStorePath, err := ioutil.TempDir("", "boltdb")
if err != nil {
@@ -36,11 +35,7 @@ func NewTestStore(init bool) (*bolt.Store, func(), error) {
return nil, nil, err
}
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
return nil, nil, err
}
store := NewStore(dataStorePath, fileService)
err = store.Open()
if err != nil {
return nil, nil, err
@@ -60,7 +55,7 @@ func NewTestStore(init bool) (*bolt.Store, func(), error) {
return store, teardown, nil
}
func teardown(store *bolt.Store, dataStorePath string) {
func teardown(store *Store, dataStorePath string) {
err := store.Close()
if err != nil {
log.Fatalln(err)

View File

@@ -15,6 +15,7 @@ const (
versionKey = "DB_VERSION"
instanceKey = "INSTANCE_ID"
editionKey = "EDITION"
updatingKey = "DB_UPDATING"
)
// Service represents a service to manage stored versions.
@@ -83,6 +84,21 @@ func (service *Service) StoreDBVersion(version int) error {
})
}
// IsUpdating retrieves the database updating status.
func (service *Service) IsUpdating() (bool, error) {
isUpdating, err := service.getKey(updatingKey)
if err != nil {
return false, err
}
return strconv.ParseBool(string(isUpdating))
}
// StoreIsUpdating store the database updating status.
func (service *Service) StoreIsUpdating(isUpdating bool) error {
return service.setKey(updatingKey, strconv.FormatBool(isUpdating))
}
// InstanceID retrieves the stored instance ID.
func (service *Service) InstanceID() (string, error) {
var data []byte

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"net/http"
"strconv"
"time"
@@ -42,6 +43,55 @@ func NewService(dataStore portainer.DataStore, shutdownCtx context.Context) *Ser
}
}
// pingAgent ping the given agent so that the agent can keep the tunnel alive
func (service *Service) pingAgent(endpointID portainer.EndpointID) error{
tunnel := service.GetTunnelDetails(endpointID)
requestURL := fmt.Sprintf("http://127.0.0.1:%d/ping", tunnel.Port)
req, err := http.NewRequest(http.MethodHead, requestURL, nil)
if err != nil {
return err
}
httpClient := &http.Client{
Timeout: 3 * time.Second,
}
_, err = httpClient.Do(req)
if err != nil {
return err
}
return nil
}
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
func (service *Service) KeepTunnelAlive(endpointID portainer.EndpointID, ctx context.Context, maxAlive time.Duration) {
go func() {
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: start for %.0f minutes]\n", endpointID, maxAlive.Minutes())
maxAliveTicker := time.NewTicker(maxAlive)
defer maxAliveTicker.Stop()
pingTicker := time.NewTicker(tunnelCleanupInterval)
defer pingTicker.Stop()
for {
select {
case <-pingTicker.C:
service.SetTunnelStatusToActive(endpointID)
err := service.pingAgent(endpointID)
if err != nil {
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [warning: ping agent err=%s]\n", endpointID, err)
}
case <-maxAliveTicker.C:
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: stop as %.0f minutes timeout]\n", endpointID, maxAlive.Minutes())
return
case <-ctx.Done():
err := ctx.Err()
log.Printf("[DEBUG] [chisel,KeepTunnelAlive] [endpoint_id: %d] [message: stop as err=%s]\n", endpointID, err)
return
}
}
}()
}
// StartTunnelServer starts a tunnel server on the specified addr and port.
// It uses a seed to generate a new private/public key pair. If the seed cannot
// be found inside the database, it will generate a new one randomly and persist it.

View File

@@ -56,6 +56,32 @@ func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *porta
}
}
// GetActiveTunnel retrieves an active tunnel which allows communicating with edge agent
func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portainer.TunnelDetails, error) {
tunnel := service.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return nil, fmt.Errorf("failed opening tunnel to endpoint: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := service.dataStore.Settings().Settings()
if err != nil {
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
}
tunnel = service.GetTunnelDetails(endpoint.ID)
return tunnel, nil
}
// SetTunnelStatusToActive update the status of the tunnel associated to the specified environment(endpoint).
// It sets the status to ACTIVE.
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {

View File

@@ -47,6 +47,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL (deprecated)").Default(defaultSSL).Bool(),
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").String(),
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").Default(defaultSnapshotInterval).String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),

24
api/cli/confirm.go Normal file
View File

@@ -0,0 +1,24 @@
package cli
import (
"bufio"
"log"
"os"
"strings"
)
// Confirm starts a rollback db cli application
func Confirm(message string) (bool, error) {
log.Printf("%s [y/N]", message)
reader := bufio.NewReader(os.Stdin)
answer, err := reader.ReadString('\n')
if err != nil {
return false, err
}
answer = strings.Replace(answer, "\n", "", -1)
answer = strings.ToLower(answer)
return answer == "y" || answer == "yes", nil
}

View File

@@ -56,17 +56,24 @@ func initFileService(dataStorePath string) portainer.FileService {
return fileService
}
func initDataStore(dataStorePath string, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
store, err := bolt.NewStore(dataStorePath, fileService)
if err != nil {
log.Fatalf("failed creating data store: %v", err)
}
err = store.Open()
func initDataStore(dataStorePath string, rollback bool, fileService portainer.FileService, shutdownCtx context.Context) portainer.DataStore {
store := bolt.NewStore(dataStorePath, fileService)
err := store.Open()
if err != nil {
log.Fatalf("failed opening store: %v", err)
}
if rollback {
err := store.Rollback(false)
if err != nil {
log.Fatalf("failed rolling back: %s", err)
}
log.Println("Exiting rollback")
os.Exit(0)
return nil
}
err = store.Init()
if err != nil {
log.Fatalf("failed initializing data store: %v", err)
@@ -99,8 +106,8 @@ func initSwarmStackManager(assetsPath string, configPath string, signatureServic
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService)
}
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath)
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager, assetsPath)
}
func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, error) {
@@ -399,7 +406,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
fileService := initFileService(*flags.Data)
dataStore := initDataStore(*flags.Data, fileService, shutdownCtx)
dataStore := initDataStore(*flags.Data, *flags.Rollback, fileService, shutdownCtx)
if err := dataStore.CheckCurrentEdition(); err != nil {
log.Fatal(err)
@@ -469,7 +476,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatalf("failed initializing swarm stack manager: %s", err)
}
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets)
kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, proxyManager, *flags.Assets)
helmPackageManager, err := initHelmPackageManager(*flags.Assets)
if err != nil {
@@ -542,7 +549,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
scheduler := scheduler.NewScheduler(shutdownCtx)
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager)
stackDeployer := stacks.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer)
stacks.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
return &http.Server{

5
api/exec/common.go Normal file
View File

@@ -0,0 +1,5 @@
package exec
import "regexp"
var stackNameNormalizeRegex = regexp.MustCompile("[^-_a-z0-9]+")

View File

@@ -6,7 +6,6 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/errors"
@@ -47,7 +46,7 @@ func (manager *ComposeStackManager) ComposeSyntaxMaxVersion() string {
func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
url, proxy, err := manager.fetchEndpointProxy(endpoint)
if err != nil {
return errors.Wrap(err, "failed to featch environment proxy")
return errors.Wrap(err, "failed to fetch environment proxy")
}
if proxy != nil {
@@ -80,9 +79,8 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
}
// NormalizeStackName returns a new stack name with unsupported characters replaced
func (w *ComposeStackManager) NormalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
func (manager *ComposeStackManager) NormalizeStackName(name string) string {
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
}
func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
@@ -90,7 +88,7 @@ func (manager *ComposeStackManager) fetchEndpointProxy(endpoint *portainer.Endpo
return "", nil, nil
}
proxy, err := manager.proxyManager.CreateComposeProxyServer(endpoint)
proxy, err := manager.proxyManager.CreateAgentProxyServer(endpoint)
if err != nil {
return "", nil, err
}

View File

@@ -0,0 +1,23 @@
package exectest
import (
portainer "github.com/portainer/portainer/api"
)
type kubernetesMockDeployer struct{}
func NewKubernetesDeployer() portainer.KubernetesDeployer {
return &kubernetesMockDeployer{}
}
func (deployer *kubernetesMockDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
return "", nil
}
func (deployer *kubernetesMockDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
return "", nil
}
func (deployer *kubernetesMockDeployer) ConvertCompose(data []byte) ([]byte, error) {
return nil, nil
}

View File

@@ -2,24 +2,19 @@ package exec
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os/exec"
"path"
"runtime"
"strings"
"time"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/cli"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
)
// KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment(endpoint).
@@ -30,10 +25,11 @@ type KubernetesDeployer struct {
signatureService portainer.DigitalSignatureService
kubernetesClientFactory *cli.ClientFactory
kubernetesTokenCacheManager *kubernetes.TokenCacheManager
proxyManager *proxy.Manager
}
// NewKubernetesDeployer initializes a new KubernetesDeployer service.
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer {
func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, binaryPath string) *KubernetesDeployer {
return &KubernetesDeployer{
binaryPath: binaryPath,
dataStore: datastore,
@@ -41,32 +37,33 @@ func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheMan
signatureService: signatureService,
kubernetesClientFactory: kubernetesClientFactory,
kubernetesTokenCacheManager: kubernetesTokenCacheManager,
proxyManager: proxyManager,
}
}
func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
tokenData, err := security.RetrieveTokenData(request)
if err != nil {
return "", err
}
kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
func (deployer *KubernetesDeployer) getToken(userID portainer.UserID, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) {
kubeCLI, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return "", err
}
tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID))
tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken)
tokenManager, err := kubernetes.NewTokenManager(kubeCLI, deployer.dataStore, tokenCache, setLocalAdminToken)
if err != nil {
return "", err
}
if tokenData.Role == portainer.AdministratorRole {
user, err := deployer.dataStore.User().User(userID)
if err != nil {
return "", errors.Wrap(err, "failed to fetch the user")
}
if user.Role == portainer.AdministratorRole {
return tokenManager.GetAdminServiceAccountToken(), nil
}
token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID), endpoint.ID)
token, err := tokenManager.GetUserServiceAccountToken(int(user.ID), endpoint.ID)
if err != nil {
return "", err
}
@@ -77,156 +74,62 @@ func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *po
return token, nil
}
// Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes environment(endpoint).
// Otherwise it will use kubectl to deploy the manifest.
func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) {
if endpoint.Type == portainer.KubernetesLocalEnvironment {
token, err := deployer.getToken(request, endpoint, true)
if err != nil {
return "", err
}
// Deploy upserts Kubernetes resources defined in manifest(s)
func (deployer *KubernetesDeployer) Deploy(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
return deployer.command("apply", userID, endpoint, manifestFiles, namespace)
}
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
// Remove deletes Kubernetes resources defined in manifest(s)
func (deployer *KubernetesDeployer) Remove(userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
return deployer.command("delete", userID, endpoint, manifestFiles, namespace)
}
args := make([]string, 0)
args = append(args, "--server", endpoint.URL)
args = append(args, "--insecure-skip-tls-verify")
args = append(args, "--token", token)
func (deployer *KubernetesDeployer) command(operation string, userID portainer.UserID, endpoint *portainer.Endpoint, manifestFiles []string, namespace string) (string, error) {
token, err := deployer.getToken(userID, endpoint, endpoint.Type == portainer.KubernetesLocalEnvironment)
if err != nil {
return "", errors.Wrap(err, "failed generating a user token")
}
command := path.Join(deployer.binaryPath, "kubectl")
if runtime.GOOS == "windows" {
command = path.Join(deployer.binaryPath, "kubectl.exe")
}
args := []string{"--token", token}
if namespace != "" {
args = append(args, "--namespace", namespace)
args = append(args, "apply", "-f", "-")
}
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
cmd.Stdin = strings.NewReader(stackConfig)
output, err := cmd.Output()
if endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
url, proxy, err := deployer.getAgentURL(endpoint)
if err != nil {
return "", errors.New(stderr.String())
return "", errors.WithMessage(err, "failed generating endpoint URL")
}
return string(output), nil
defer proxy.Close()
args = append(args, "--server", url)
args = append(args, "--insecure-skip-tls-verify")
}
// agent
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
tunnel := deployer.reverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
err := deployer.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return "", err
}
settings, err := deployer.dataStore.Settings().Settings()
if err != nil {
return "", err
}
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
}
endpointURL = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
if operation == "delete" {
args = append(args, "--ignore-not-found=true")
}
transport := &http.Transport{}
if endpoint.TLSConfig.TLS {
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return "", err
}
transport.TLSClientConfig = tlsConfig
args = append(args, operation)
for _, path := range manifestFiles {
args = append(args, "-f", strings.TrimSpace(path))
}
httpCli := &http.Client{
Transport: transport,
}
var stderr bytes.Buffer
cmd := exec.Command(command, args...)
cmd.Stderr = &stderr
if !strings.HasPrefix(endpointURL, "http") {
endpointURL = fmt.Sprintf("https://%s", endpointURL)
}
url, err := url.Parse(fmt.Sprintf("%s/v2/kubernetes/stack", endpointURL))
output, err := cmd.Output()
if err != nil {
return "", err
return "", errors.Wrapf(err, "failed to execute kubectl command: %q", stderr.String())
}
reqPayload, err := json.Marshal(
struct {
StackConfig string
Namespace string
}{
StackConfig: stackConfig,
Namespace: namespace,
})
if err != nil {
return "", err
}
req, err := http.NewRequest(http.MethodPost, url.String(), bytes.NewReader(reqPayload))
if err != nil {
return "", err
}
signature, err := deployer.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return "", err
}
token, err := deployer.getToken(request, endpoint, false)
if err != nil {
return "", err
}
req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey())
req.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token)
resp, err := httpCli.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var errorResponseData struct {
Message string
Details string
}
err = json.NewDecoder(resp.Body).Decode(&errorResponseData)
if err != nil {
output, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed parsing, body: %s, error: %w", output, err)
}
return "", fmt.Errorf("Deployment to agent failed: %s", errorResponseData.Details)
}
var responseData struct{ Output string }
err = json.NewDecoder(resp.Body).Decode(&responseData)
if err != nil {
parsedOutput, parseStringErr := ioutil.ReadAll(resp.Body)
if parseStringErr != nil {
return "", parseStringErr
}
return "", fmt.Errorf("Failed decoding, body: %s, err: %w", parsedOutput, err)
}
return responseData.Output, nil
return string(output), nil
}
// ConvertCompose leverages the kompose binary to deploy a compose compliant manifest.
@@ -251,3 +154,12 @@ func (deployer *KubernetesDeployer) ConvertCompose(data []byte) ([]byte, error)
return output, nil
}
func (deployer *KubernetesDeployer) getAgentURL(endpoint *portainer.Endpoint) (string, *factory.ProxyServer, error) {
proxy, err := deployer.proxyManager.CreateAgentProxyServer(endpoint)
if err != nil {
return "", nil, err
}
return fmt.Sprintf("http://127.0.0.1:%d/kubernetes", proxy.Port), proxy, nil
}

View File

@@ -8,7 +8,6 @@ import (
"os"
"os/exec"
"path"
"regexp"
"runtime"
"strings"
@@ -190,8 +189,7 @@ func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (ma
}
func (manager *SwarmStackManager) NormalizeStackName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
return stackNameNormalizeRegex.ReplaceAllString(strings.ToLower(name), "")
}
func configureFilePaths(args []string, filePaths []string) []string {

23
api/filesystem/write.go Normal file
View File

@@ -0,0 +1,23 @@
package filesystem
import (
"os"
"path/filepath"
"github.com/pkg/errors"
)
func WriteToFile(dst string, content []byte) error {
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
return errors.Wrapf(err, "failed to create filestructure for the path %q", dst)
}
file, err := os.Create(dst)
if err != nil {
return errors.Wrapf(err, "failed to open a file %q", dst)
}
defer file.Close()
_, err = file.Write(content)
return errors.Wrapf(err, "failed to write a file %q", dst)
}

View File

@@ -0,0 +1,48 @@
package filesystem
import (
"io/ioutil"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_WriteFile_CanStoreContentInANewFile(t *testing.T) {
tmpDir := t.TempDir()
tmpFilePath := path.Join(tmpDir, "dummy")
content := []byte("content")
err := WriteToFile(tmpFilePath, content)
assert.NoError(t, err)
fileContent, _ := ioutil.ReadFile(tmpFilePath)
assert.Equal(t, content, fileContent)
}
func Test_WriteFile_CanOverwriteExistingFile(t *testing.T) {
tmpDir := t.TempDir()
tmpFilePath := path.Join(tmpDir, "dummy")
err := WriteToFile(tmpFilePath, []byte("content"))
assert.NoError(t, err)
content := []byte("new content")
err = WriteToFile(tmpFilePath, content)
assert.NoError(t, err)
fileContent, _ := ioutil.ReadFile(tmpFilePath)
assert.Equal(t, content, fileContent)
}
func Test_WriteFile_CanWriteANestedPath(t *testing.T) {
tmpDir := t.TempDir()
tmpFilePath := path.Join(tmpDir, "dir", "sub-dir", "dummy")
content := []byte("content")
err := WriteToFile(tmpFilePath, content)
assert.NoError(t, err)
fileContent, _ := ioutil.ReadFile(tmpFilePath)
assert.Equal(t, content, fileContent)
}

View File

@@ -38,7 +38,7 @@ require (
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
@@ -46,6 +46,7 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gotest.tools v2.2.0+incompatible // indirect

View File

@@ -206,14 +206,12 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/portainer/docker-compose-wrapper v0.0.0-20210909011155-9ff375eac059 h1:98v0k3x3ZXa09NaHP/HmSA83rcN8cuE/zTKo6xvNmoM=
github.com/portainer/docker-compose-wrapper v0.0.0-20210909011155-9ff375eac059/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1 h1:0ZGSu3Atz7RHMDsoITHV676igRfsb51mlgELGo37ELU=
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1/go.mod h1:WxDlJWZxCnicdLCPnLNEv7/gRhjeIVuCGmsv+iOPH3c=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a h1:qY8TbocN75n5PDl16o0uVr5MevtM5IhdwSelXEd4nFM=
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a/go.mod h1:n54EEIq+MM0NNtqLeCby8ljL+l275VpolXO0ibHegLE=
github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97 h1:ZcRVgWHTac8V7WU9TUBr73H3e5ajVFYTPjPl9TWULDA=
github.com/portainer/libhelm v0.0.0-20210906035629-b5635edd5d97/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA=
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108 h1:5e8KAnDa2G3cEHK7aV/ue8lOaoQwBZUzoALslwWkR04=
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108/go.mod h1:YvYAk7krKTzB+rFwDr0jQ3sQu2BtiXK1AR0sZH7nhJA=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33 h1:H8HR2dHdBf8HANSkUyVw4o8+4tegGcd+zyKZ3e599II=
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33/go.mod h1:Y2TfgviWI4rT2qaOTHr+hq6MdKIE5YjgQAu7qwptTV0=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
@@ -278,6 +276,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=

View File

@@ -3,6 +3,7 @@ package customtemplates
import (
"errors"
"net/http"
"regexp"
"strconv"
"github.com/asaskevich/govalidator"
@@ -129,9 +130,20 @@ func (payload *customTemplateFromFileContentPayload) Validate(r *http.Request) e
if payload.Type != portainer.KubernetesStack && payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
return errors.New("Invalid custom template type")
}
if !isValidNote(payload.Note) {
return errors.New("Invalid note. <img> tag is not supported")
}
return nil
}
func isValidNote(note string) bool {
if govalidator.IsNull(note) {
return true
}
match, _ := regexp.MatchString("<img", note)
return !match
}
func (handler *Handler) createCustomTemplateFromFileContent(r *http.Request) (*portainer.CustomTemplate, error) {
var payload customTemplateFromFileContentPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
@@ -218,6 +230,9 @@ func (payload *customTemplateFromGitRepositoryPayload) Validate(r *http.Request)
if payload.Type != portainer.DockerSwarmStack && payload.Type != portainer.DockerComposeStack {
return errors.New("Invalid custom template type")
}
if !isValidNote(payload.Note) {
return errors.New("Invalid note. <img> tag is not supported")
}
return nil
}
@@ -285,6 +300,9 @@ func (payload *customTemplateFromFileUploadPayload) Validate(r *http.Request) er
payload.Logo = logo
note, _ := request.RetrieveMultiPartFormValue(r, "Note", true)
if !isValidNote(note) {
return errors.New("Invalid note. <img> tag is not supported")
}
payload.Note = note
typeNumeral, _ := request.RetrieveNumericMultiPartFormValue(r, "Type", true)

View File

@@ -51,6 +51,9 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Description) {
return errors.New("Invalid custom template description")
}
if !isValidNote(payload.Note) {
return errors.New("Invalid note. <img> tag is not supported")
}
return nil
}

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.0
// @version 2.9.2
// @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

@@ -27,15 +27,17 @@ type Handler struct {
requestBouncer requestBouncer
dataStore portainer.DataStore
kubeConfigService kubernetes.KubeConfigService
kubernetesDeployer portainer.KubernetesDeployer
helmPackageManager libhelm.HelmPackageManager
}
// NewHandler creates a handler to manage environment(endpoint) group operations.
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
// NewHandler creates a handler to manage endpoint group operations.
func NewHandler(bouncer requestBouncer, dataStore portainer.DataStore, kubernetesDeployer portainer.KubernetesDeployer, helmPackageManager libhelm.HelmPackageManager, kubeConfigService kubernetes.KubeConfigService) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,
dataStore: dataStore,
kubernetesDeployer: kubernetesDeployer,
helmPackageManager: helmPackageManager,
kubeConfigService: kubeConfigService,
}

View File

@@ -9,11 +9,12 @@ import (
"github.com/portainer/libhelm/binary/test"
"github.com/portainer/libhelm/options"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/exec/exectest"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes"
"github.com/stretchr/testify/assert"
bolt "github.com/portainer/portainer/api/bolt/bolttest"
"github.com/portainer/portainer/api/bolt"
helper "github.com/portainer/portainer/api/internal/testhelpers"
)
@@ -29,9 +30,10 @@ func Test_helmDelete(t *testing.T) {
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "Error creating a user")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
is.NotNil(h, "Handler should not fail")

View File

@@ -1,18 +1,23 @@
package helm
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"github.com/pkg/errors"
"github.com/portainer/libhelm/options"
"github.com/portainer/libhelm/release"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes"
"github.com/portainer/portainer/api/kubernetes/validation"
"golang.org/x/sync/errgroup"
)
type installChartPayload struct {
@@ -131,5 +136,98 @@ func (handler *Handler) installChart(r *http.Request, p installChartPayload) (*r
if err != nil {
return nil, err
}
manifest, err := handler.applyPortainerLabelsToHelmAppManifest(r, installOpts, release.Manifest)
if err != nil {
return nil, err
}
err = handler.updateHelmAppManifest(r, manifest, installOpts.Namespace)
if err != nil {
return nil, err
}
return release, nil
}
// applyPortainerLabelsToHelmAppManifest will patch all the resources deployed in the helm release manifest
// with portainer specific labels. This is to mark the resources as managed by portainer - hence the helm apps
// wont appear external in the portainer UI.
func (handler *Handler) applyPortainerLabelsToHelmAppManifest(r *http.Request, installOpts options.InstallOptions, manifest string) ([]byte, error) {
// Patch helm release by adding with portainer labels to all deployed resources
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return nil, errors.Wrap(err, "unable to retrieve user details from authentication token")
}
user, err := handler.dataStore.User().User(tokenData.ID)
if err != nil {
return nil, errors.Wrap(err, "unable to load user information from the database")
}
appLabels := kubernetes.GetHelmAppLabels(installOpts.Name, user.Username)
labeledManifest, err := kubernetes.AddAppLabels([]byte(manifest), appLabels)
if err != nil {
return nil, errors.Wrap(err, "failed to label helm release manifest")
}
return labeledManifest, nil
}
// updateHelmAppManifest will update the resources of helm release manifest with portainer labels using kubectl.
// The resources of the manifest will be updated in parallel and individuallly since resources of a chart
// can be deployed to different namespaces.
// NOTE: These updates will need to be re-applied when upgrading the helm release
func (handler *Handler) updateHelmAppManifest(r *http.Request, manifest []byte, namespace string) error {
endpoint, err := middlewares.FetchEndpoint(r)
if err != nil {
return errors.Wrap(err, "unable to find an endpoint on request context")
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return errors.Wrap(err, "unable to retrieve user details from authentication token")
}
// extract list of yaml resources from helm manifest
yamlResources, err := kubernetes.ExtractDocuments(manifest, nil)
if err != nil {
return errors.Wrap(err, "unable to extract documents from helm release manifest")
}
// deploy individual resources in parallel
g := new(errgroup.Group)
for _, resource := range yamlResources {
resource := resource // https://golang.org/doc/faq#closures_and_goroutines
g.Go(func() error {
tmpfile, err := ioutil.TempFile("", "helm-manifest-*")
if err != nil {
return errors.Wrap(err, "failed to create a tmp helm manifest file")
}
defer func() {
tmpfile.Close()
os.Remove(tmpfile.Name())
}()
if _, err := tmpfile.Write(resource); err != nil {
return errors.Wrap(err, "failed to write a tmp helm manifest file")
}
// get resource namespace, fallback to provided namespace if not explicit on resource
resourceNamespace, err := kubernetes.GetNamespace(resource)
if err != nil {
return err
}
if resourceNamespace == "" {
resourceNamespace = namespace
}
_, err = handler.kubernetesDeployer.Deploy(tokenData.ID, endpoint, []string{tmpfile.Name()}, resourceNamespace)
return err
})
}
if err := g.Wait(); err != nil {
return errors.Wrap(err, "unable to patch helm release using kubectl")
}
return nil
}

View File

@@ -12,7 +12,8 @@ import (
"github.com/portainer/libhelm/options"
"github.com/portainer/libhelm/release"
portainer "github.com/portainer/portainer/api"
bolt "github.com/portainer/portainer/api/bolt/bolttest"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/exec/exectest"
"github.com/portainer/portainer/api/http/security"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/portainer/portainer/api/kubernetes"
@@ -31,9 +32,10 @@ func Test_helmInstall(t *testing.T) {
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
is.NotNil(h, "Handler should not fail")

View File

@@ -11,10 +11,11 @@ import (
"github.com/portainer/libhelm/options"
"github.com/portainer/libhelm/release"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/exec/exectest"
"github.com/portainer/portainer/api/kubernetes"
"github.com/stretchr/testify/assert"
bolt "github.com/portainer/portainer/api/bolt/bolttest"
"github.com/portainer/portainer/api/bolt"
helper "github.com/portainer/portainer/api/internal/testhelpers"
)
@@ -28,9 +29,10 @@ func Test_helmList(t *testing.T) {
err = store.User().CreateUser(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
assert.NoError(t, err, "error creating a user")
kubernetesDeployer := exectest.NewKubernetesDeployer()
helmPackageManager := test.NewMockHelmBinaryPackageManager("")
kubeConfigService := kubernetes.NewKubeConfigCAService("", "")
h := NewHandler(helper.NewTestRequestBouncer(), store, helmPackageManager, kubeConfigService)
h := NewHandler(helper.NewTestRequestBouncer(), store, kubernetesDeployer, helmPackageManager, kubeConfigService)
// Install a single chart. We expect to get these values back
options := options.InstallOptions{Name: "nginx-1", Chart: "nginx", Namespace: "default"}

View File

@@ -6,7 +6,6 @@ import (
"net/url"
"github.com/pkg/errors"
"github.com/portainer/libhelm"
"github.com/portainer/libhelm/options"
httperror "github.com/portainer/libhttp/error"
)
@@ -40,7 +39,7 @@ func (handler *Handler) helmRepoSearch(w http.ResponseWriter, r *http.Request) *
Repo: repo,
}
result, err := libhelm.SearchRepo(searchOpts)
result, err := handler.helmPackageManager.SearchRepo(searchOpts)
if err != nil {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,

View File

@@ -9,13 +9,12 @@ import (
"testing"
"github.com/portainer/libhelm/binary/test"
"github.com/stretchr/testify/assert"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
func Test_helmRepoSearch(t *testing.T) {
helper.IntegrationTest(t)
is := assert.New(t)
helmPackageManager := test.NewMockHelmBinaryPackageManager("")

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

@@ -40,6 +40,8 @@ type settingsUpdatePayload struct {
EnableTelemetry *bool `example:"false"`
// Helm repository URL
HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"`
// Kubectl Shell Image
KubectlShellImage *string `example:"portainer/kubectl-shell:latest"`
}
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
@@ -52,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)
@@ -112,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 {
@@ -178,6 +186,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
return tlsError
}
if payload.KubectlShellImage != nil {
settings.KubectlShellImage = *payload.KubectlShellImage
}
err = handler.DataStore.Settings().UpdateSettings(settings)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err}

View File

@@ -17,10 +17,8 @@ func startAutoupdate(stackID portainer.StackID, interval string, scheduler *sche
return "", &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Unable to parse stack's auto update interval", Err: err}
}
jobID = scheduler.StartJobEvery(d, func() {
if err := stacks.RedeployWhenChanged(stackID, stackDeployer, datastore, gitService); err != nil {
log.Printf("[ERROR] [http,stacks] [message: failed redeploying] [err: %s]\n", err)
}
jobID = scheduler.StartJobEvery(d, func() error {
return stacks.RedeployWhenChanged(stackID, stackDeployer, datastore, gitService)
})
return jobID, nil

View File

@@ -46,13 +46,12 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, false)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
return stackExistsError(payload.Name)
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@@ -152,12 +151,12 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
payload.ComposeFile = filesystem.ComposeFileDefaultName
}
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, false)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), Err: errStackAlreadyExists}
return stackExistsError(payload.Name)
}
//make sure the webhook ID is unique
@@ -208,11 +207,11 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
}
commitId, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
commitID, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
}
stack.GitConfig.ConfigHash = commitId
stack.GitConfig.ConfigHash = commitID
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
if configErr != nil {
@@ -281,13 +280,12 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
payload.Name = handler.ComposeStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, false)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, false)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' already exists", payload.Name)
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
return stackExistsError(payload.Name)
}
stackID := handler.DataStore.Stack().GetNextIdentifier()

View File

@@ -2,9 +2,8 @@ package stacks
import (
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"os"
"strconv"
"time"
@@ -19,16 +18,19 @@ import (
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/internal/stackutils"
k "github.com/portainer/portainer/api/kubernetes"
)
type kubernetesStringDeploymentPayload struct {
StackName string
ComposeFormat bool
Namespace string
StackFileContent string
}
type kubernetesGitDeploymentPayload struct {
StackName string
ComposeFormat bool
Namespace string
RepositoryURL string
@@ -36,10 +38,13 @@ type kubernetesGitDeploymentPayload struct {
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
FilePathInRepository string
ManifestFile string
AdditionalFiles []string
AutoUpdate *portainer.StackAutoUpdate
}
type kubernetesManifestURLDeploymentPayload struct {
StackName string
Namespace string
ComposeFormat bool
ManifestURL string
@@ -52,6 +57,9 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
if govalidator.IsNull(payload.Namespace) {
return errors.New("Invalid namespace")
}
if govalidator.IsNull(payload.StackName) {
return errors.New("Invalid stack name")
}
return nil
}
@@ -65,12 +73,18 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
if payload.RepositoryAuthentication && govalidator.IsNull(payload.RepositoryPassword) {
return errors.New("Invalid repository credentials. Password must be specified when authentication is enabled")
}
if govalidator.IsNull(payload.FilePathInRepository) {
return errors.New("Invalid file path in repository")
if govalidator.IsNull(payload.ManifestFile) {
return errors.New("Invalid manifest file in repository")
}
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
}
if govalidator.IsNull(payload.StackName) {
return errors.New("Invalid stack name")
}
return nil
}
@@ -78,6 +92,9 @@ func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request)
if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) {
return errors.New("Invalid manifest URL")
}
if govalidator.IsNull(payload.StackName) {
return errors.New("Invalid stack name")
}
return nil
}
@@ -95,6 +112,13 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: errStackAlreadyExists}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
@@ -102,6 +126,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Name: payload.StackName,
Namespace: payload.Namespace,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
@@ -124,11 +149,11 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
output, err := handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
StackID: stackID,
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "content",
output, err := handler.deployKubernetesStack(user.ID, endpoint, stack, k.KubeAppLabels{
StackID: stackID,
StackName: stack.Name,
Owner: stack.CreatedBy,
Kind: "content",
})
if err != nil {
@@ -140,12 +165,11 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err}
}
doCleanUp = false
resp := &createKubernetesStackResponse{
Output: output,
}
doCleanUp = false
return response.JSON(w, resp)
}
@@ -159,23 +183,44 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: errStackAlreadyExists}
}
//make sure the webhook ID is unique
if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" {
isUnique, err := handler.checkUniqueWebhookID(payload.AutoUpdate.Webhook)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for webhook ID collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("Webhook ID: %s already exists", payload.AutoUpdate.Webhook), Err: errWebhookIDAlreadyExists}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
ID: portainer.StackID(stackID),
Type: portainer.KubernetesStack,
EndpointID: endpoint.ID,
EntryPoint: payload.FilePathInRepository,
EntryPoint: payload.ManifestFile,
GitConfig: &gittypes.RepoConfig{
URL: payload.RepositoryURL,
ReferenceName: payload.RepositoryReferenceName,
ConfigFilePath: payload.FilePathInRepository,
ConfigFilePath: payload.ManifestFile,
},
Namespace: payload.Namespace,
Name: payload.StackName,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
CreatedBy: user.Username,
IsComposeFormat: payload.ComposeFormat,
AutoUpdate: payload.AutoUpdate,
AdditionalFiles: payload.AdditionalFiles,
}
if payload.RepositoryAuthentication {
@@ -197,33 +242,48 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr
}
stack.GitConfig.ConfigHash = commitID
stackFileContent, err := handler.cloneManifestContentFromGitRepo(&payload, stack.ProjectPath)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err}
repositoryUsername := payload.RepositoryUsername
repositoryPassword := payload.RepositoryPassword
if !payload.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
StackID: stackID,
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "git",
err = handler.GitService.CloneRepository(projectPath, payload.RepositoryURL, payload.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to clone git repository", Err: err}
}
output, err := handler.deployKubernetesStack(user.ID, endpoint, stack, k.KubeAppLabels{
StackID: stackID,
StackName: stack.Name,
Owner: stack.CreatedBy,
Kind: "git",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
}
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
stack.AutoUpdate.JobID = jobID
}
err = handler.DataStore.Stack().CreateStack(stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack inside the database", Err: err}
}
doCleanUp = false
resp := &createKubernetesStackResponse{
Output: output,
}
doCleanUp = false
return response.JSON(w, resp)
}
@@ -237,6 +297,13 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to load user information from the database", Err: err}
}
isUnique, err := handler.checkUniqueStackName(endpoint, payload.StackName, 0)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.StackName), Err: errStackAlreadyExists}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
stack := &portainer.Stack{
@@ -245,6 +312,7 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
EndpointID: endpoint.ID,
EntryPoint: filesystem.ManifestFileDefaultName,
Namespace: payload.Namespace,
Name: payload.StackName,
Status: portainer.StackStatusActive,
CreationDate: time.Now().Unix(),
CreatedBy: user.Username,
@@ -267,11 +335,11 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
doCleanUp := true
defer handler.cleanUp(stack, &doCleanUp)
output, err := handler.deployKubernetesStack(r, endpoint, string(manifestContent), payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{
StackID: stackID,
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "url",
output, err := handler.deployKubernetesStack(user.ID, endpoint, stack, k.KubeAppLabels{
StackID: stackID,
StackName: stack.Name,
Owner: stack.CreatedBy,
Kind: "url",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err}
@@ -291,42 +359,14 @@ func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWrit
return response.JSON(w, resp)
}
func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string, appLabels k.KubeAppLabels) (string, error) {
func (handler *Handler) deployKubernetesStack(userID portainer.UserID, endpoint *portainer.Endpoint, stack *portainer.Stack, appLabels k.KubeAppLabels) (string, error) {
handler.stackCreationMutex.Lock()
defer handler.stackCreationMutex.Unlock()
manifest := []byte(stackConfig)
if composeFormat {
convertedConfig, err := handler.KubernetesDeployer.ConvertCompose(manifest)
if err != nil {
return "", errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
}
manifest = convertedConfig
}
manifest, err := k.AddAppLabels(manifest, appLabels)
manifestFilePaths, tempDir, err := stackutils.CreateTempK8SDeploymentFiles(stack, handler.KubernetesDeployer, appLabels)
if err != nil {
return "", errors.Wrap(err, "failed to add application labels")
return "", errors.Wrap(err, "failed to create temp kub deployment files")
}
return handler.KubernetesDeployer.Deploy(request, endpoint, string(manifest), namespace)
}
func (handler *Handler) cloneManifestContentFromGitRepo(gitInfo *kubernetesGitDeploymentPayload, projectPath string) (string, error) {
repositoryUsername := gitInfo.RepositoryUsername
repositoryPassword := gitInfo.RepositoryPassword
if !gitInfo.RepositoryAuthentication {
repositoryUsername = ""
repositoryPassword = ""
}
err := handler.GitService.CloneRepository(projectPath, gitInfo.RepositoryURL, gitInfo.RepositoryReferenceName, repositoryUsername, repositoryPassword)
if err != nil {
return "", err
}
content, err := ioutil.ReadFile(filepath.Join(projectPath, gitInfo.FilePathInRepository))
if err != nil {
return "", err
}
return string(content), nil
defer os.RemoveAll(tempDir)
return handler.KubernetesDeployer.Deploy(userID, endpoint, manifestFilePaths, stack.Namespace)
}

View File

@@ -1,68 +0,0 @@
package stacks
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
type git struct {
content string
}
func (g *git) CloneRepository(destination string, repositoryURL, referenceName, username, password string) error {
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
}
func (g *git) ClonePublicRepository(repositoryURL string, referenceName string, destination string) error {
return ioutil.WriteFile(path.Join(destination, "deployment.yml"), []byte(g.content), 0755)
}
func (g *git) ClonePrivateRepositoryWithBasicAuth(repositoryURL, referenceName string, destination, username, password string) error {
return g.ClonePublicRepository(repositoryURL, referenceName, destination)
}
func (g *git) LatestCommitID(repositoryURL, referenceName, username, password string) (string, error) {
return "", nil
}
func TestCloneAndConvertGitRepoFile(t *testing.T) {
dir, err := os.MkdirTemp("", "kube-create-stack")
assert.NoError(t, err, "failed to create a tmp dir")
defer os.RemoveAll(dir)
content := `apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80`
h := &Handler{
GitService: &git{
content: content,
},
}
gitInfo := &kubernetesGitDeploymentPayload{
FilePathInRepository: "deployment.yml",
}
fileContent, err := h.cloneManifestContentFromGitRepo(gitInfo, dir)
assert.NoError(t, err, "failed to clone or convert the file from Git repo")
assert.Equal(t, content, fileContent)
}

View File

@@ -51,13 +51,13 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, true)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
return stackExistsError(payload.Name)
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@@ -161,12 +161,12 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, true)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: fmt.Sprintf("A stack with the name '%s' already exists", payload.Name), Err: errStackAlreadyExists}
return stackExistsError(payload.Name)
}
//make sure the webhook ID is unique
@@ -218,11 +218,11 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to clone git repository", Err: err}
}
commitId, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
commitID, err := handler.latestCommitID(payload.RepositoryURL, payload.RepositoryReferenceName, payload.RepositoryAuthentication, payload.RepositoryUsername, payload.RepositoryPassword)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to fetch git repository id", Err: err}
}
stack.GitConfig.ConfigHash = commitId
stack.GitConfig.ConfigHash = commitID
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, false)
if configErr != nil {
@@ -298,13 +298,13 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
payload.Name = handler.SwarmStackManager.NormalizeStackName(payload.Name)
isUnique, err := handler.checkUniqueName(endpoint, payload.Name, 0, true)
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, payload.Name, 0, true)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", payload.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
return stackExistsError(payload.Name)
}
stackID := handler.DataStore.Stack().GetNextIdentifier()

View File

@@ -45,6 +45,12 @@ type Handler struct {
StackDeployer stacks.StackDeployer
}
func stackExistsError(name string) (*httperror.HandlerError){
msg := fmt.Sprintf("A stack with the normalized name '%s' already exists", name)
err := errors.New(msg)
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: msg, Err: err}
}
// NewHandler creates a handler to manage stack operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
@@ -127,7 +133,7 @@ func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedR
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
}
func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
func (handler *Handler) checkUniqueStackName(endpoint *portainer.Endpoint, name string, stackID portainer.StackID) (bool, error) {
stacks, err := handler.DataStore.Stack().Stacks()
if err != nil {
return false, err
@@ -139,6 +145,15 @@ func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name strin
}
}
return true, nil
}
func (handler *Handler) checkUniqueStackNameInDocker(endpoint *portainer.Endpoint, name string, stackID portainer.StackID, swarmMode bool) (bool, error) {
isUniqueStackName, err := handler.checkUniqueStackName(endpoint, name, stackID)
if err != nil {
return false, err
}
dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "")
if err != nil {
return false, err
@@ -171,7 +186,7 @@ func (handler *Handler) checkUniqueName(endpoint *portainer.Endpoint, name strin
}
}
return true, nil
return isUniqueStackName, nil
}
func (handler *Handler) checkUniqueWebhookID(webhookID string) (bool, error) {

View File

@@ -2,15 +2,20 @@ package stacks
import (
"context"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strconv"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/filesystem"
httperrors "github.com/portainer/portainer/api/http/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
@@ -34,12 +39,12 @@ import (
func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
externalStack, _ := request.RetrieveBooleanQueryParameter(r, "external", true)
@@ -49,52 +54,52 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
id, err := strconv.Atoi(stackID)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(id))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
}
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
}
isOrphaned := portainer.EndpointID(endpointID) != stack.EndpointID
if isOrphaned && !securityContext.IsAdmin {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to remove orphaned stack", errors.New("Permission denied to remove orphaned stack")}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to remove orphaned stack", Err: errors.New("Permission denied to remove orphaned stack")}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
if !isOrphaned {
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
}
}
@@ -104,26 +109,26 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
}
err = handler.deleteStack(stack, endpoint)
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
err = handler.DataStore.Stack().DeleteStack(portainer.StackID(id))
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the stack from the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove the stack from the database", Err: err}
}
if resourceControl != nil {
err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the associated resource control from the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove the associated resource control from the database", Err: err}
}
}
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to remove stack files from disk", Err: err}
}
return response.Empty(w)
@@ -132,31 +137,31 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWriter, stackName string, securityContext *security.RestrictedRequestContext) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
}
if !securityContext.IsAdmin {
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
return &httperror.HandlerError{StatusCode: http.StatusUnauthorized, Message: "Permission denied to delete the stack", Err: httperrors.ErrUnauthorized}
}
stack, err := handler.DataStore.Stack().StackByName(stackName)
if err != nil && err != bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for stack existence inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for stack existence inside the database", Err: err}
}
if stack != nil {
return &httperror.HandlerError{http.StatusBadRequest, "A stack with this name exists inside the database. Cannot use external delete method", errors.New("A tag already exists with this name")}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "A stack with this name exists inside the database. Cannot use external delete method", Err: errors.New("A tag already exists with this name")}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find the environment associated to the stack inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the environment associated to the stack inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find the endpoint associated to the stack inside the database", Err: err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
stack = &portainer.Stack{
@@ -164,18 +169,57 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
Type: portainer.DockerSwarmStack,
}
err = handler.deleteStack(stack, endpoint)
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete stack", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to delete stack", Err: err}
}
return response.Empty(w)
}
func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
func (handler *Handler) deleteStack(userID portainer.UserID, stack *portainer.Stack, endpoint *portainer.Endpoint) error {
if stack.Type == portainer.DockerSwarmStack {
return handler.SwarmStackManager.Remove(stack, endpoint)
}
if stack.Type == portainer.DockerComposeStack {
return handler.ComposeStackManager.Down(context.TODO(), stack, endpoint)
}
if stack.Type == portainer.KubernetesStack {
var manifestFiles []string
return handler.ComposeStackManager.Down(context.TODO(), stack, endpoint)
//if it is a compose format kub stack, create a temp dir and convert the manifest files into it
//then process the remove operation
if stack.IsComposeFormat {
fileNames := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
tmpDir, err := ioutil.TempDir("", "kub_delete")
if err != nil {
return errors.Wrap(err, "failed to create temp directory for deleting kub stack")
}
defer os.RemoveAll(tmpDir)
for _, fileName := range fileNames {
manifestFilePath := path.Join(tmpDir, fileName)
manifestContent, err := ioutil.ReadFile(path.Join(stack.ProjectPath, fileName))
if err != nil {
return errors.Wrap(err, "failed to read manifest file")
}
manifestContent, err = handler.KubernetesDeployer.ConvertCompose(manifestContent)
if err != nil {
return errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
}
err = filesystem.WriteToFile(manifestFilePath, []byte(manifestContent))
if err != nil {
return errors.Wrap(err, "failed to create temp manifest file")
}
manifestFiles = append(manifestFiles, manifestFilePath)
}
} else {
manifestFiles = stackutils.GetStackFilePaths(stack)
}
out, err := handler.KubernetesDeployer.Remove(userID, endpoint, manifestFiles, stack.Namespace)
return errors.WithMessagef(err, "failed to remove kubernetes resources: %q", out)
}
return fmt.Errorf("unsupported stack type: %v", stack.Type)
}

View File

@@ -50,52 +50,54 @@ func (payload *stackMigratePayload) Validate(r *http.Request) error {
func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
}
var payload stackMigratePayload
err = request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
}
if stack.Type == portainer.KubernetesStack {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Migrating a kubernetes stack is not supported", Err: err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
@@ -103,7 +105,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
// can use the optional EndpointID query parameter to associate a valid environment(endpoint) identifier to the stack.
endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", true)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid query parameter: endpointId", Err: err}
}
if endpointID != int(stack.EndpointID) {
stack.EndpointID = portainer.EndpointID(endpointID)
@@ -111,9 +113,9 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
targetEndpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(payload.EndpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
}
stack.EndpointID = portainer.EndpointID(payload.EndpointID)
@@ -126,14 +128,14 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
stack.Name = payload.Name
}
isUnique, err := handler.checkUniqueName(targetEndpoint, stack.Name, stack.ID, stack.SwarmID != "")
isUnique, err := handler.checkUniqueStackNameInDocker(targetEndpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on environment '%s'", stack.Name, targetEndpoint.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running on endpoint '%s'", stack.Name, targetEndpoint.Name)
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
}
migrationError := handler.migrateStack(r, stack, targetEndpoint)
@@ -142,14 +144,14 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
}
stack.Name = oldName
err = handler.deleteStack(stack, endpoint)
err = handler.deleteStack(securityContext.UserID, stack, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack changes inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the stack changes inside the database", Err: err}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {
@@ -175,7 +177,7 @@ func (handler *Handler) migrateComposeStack(r *http.Request, stack *portainer.St
err := handler.deployComposeStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
return nil
@@ -189,7 +191,7 @@ func (handler *Handler) migrateSwarmStack(r *http.Request, stack *portainer.Stac
err := handler.deploySwarmStack(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
}
return nil

View File

@@ -33,59 +33,61 @@ import (
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid stack identifier route variable", Err: err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find a stack with the specified identifier inside the database", Err: err}
}
if stack.Type == portainer.KubernetesStack {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Starting a kubernetes stack is not supported", Err: err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusNotFound, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to find an endpoint with the specified identifier inside the database", Err: err}
}
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access endpoint", Err: err}
}
isUnique, err := handler.checkUniqueName(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
isUnique, err := handler.checkUniqueStackNameInDocker(endpoint, stack.Name, stack.ID, stack.SwarmID != "")
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
errorMessage := fmt.Sprintf("A stack with the name '%s' is already running", stack.Name)
return &httperror.HandlerError{http.StatusConflict, errorMessage, errors.New(errorMessage)}
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: errorMessage, Err: errors.New(errorMessage)}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
}
if !access {
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Access denied to resource", Err: httperrors.ErrResourceAccessDenied}
}
if stack.Status == portainer.StackStatusActive {
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already active", errors.New("Stack is already active")}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Stack is already active", Err: errors.New("Stack is already active")}
}
if stack.AutoUpdate != nil && stack.AutoUpdate.Interval != "" {
@@ -101,13 +103,13 @@ func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *http
err = handler.startStack(stack, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start stack", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to start stack", Err: err}
}
stack.Status = portainer.StackStatusActive
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to update stack status", Err: err}
}
if stack.GitConfig != nil && stack.GitConfig.Authentication != nil && stack.GitConfig.Authentication.Password != "" {

View File

@@ -46,6 +46,10 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
}
if stack.Type == portainer.KubernetesStack {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Stopping a kubernetes stack is not supported", Err: err}
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
@@ -58,19 +62,17 @@ func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httpe
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
}
if !access {
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
}
if stack.Status == portainer.StackStatusInactive {

View File

@@ -1,10 +1,11 @@
package stacks
import (
"errors"
"net/http"
"time"
"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
@@ -98,17 +99,22 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "Permission denied to access environment", Err: err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
user, err := handler.DataStore.User().User(securityContext.UserID)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Cannot find context user", Err: errors.Wrap(err, "failed to fetch the user")}
}
if stack.Type == portainer.DockerSwarmStack || stack.Type == portainer.DockerComposeStack {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve a resource control associated to the stack", Err: err}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to retrieve info from request context", Err: err}
}
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to verify user authorizations to validate stack access", Err: err}
@@ -127,6 +133,8 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
stack.AutoUpdate = payload.AutoUpdate
stack.Env = payload.Env
stack.UpdatedBy = user.Username
stack.UpdateDate = time.Now().Unix()
stack.GitConfig.Authentication = nil
if payload.RepositoryAuthentication {

View File

@@ -2,10 +2,8 @@ package stacks
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"path/filepath"
"time"
"github.com/asaskevich/govalidator"
@@ -216,15 +214,15 @@ func (handler *Handler) deployStack(r *http.Request, stack *portainer.Stack, end
if stack.Namespace == "" {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Invalid namespace", Err: errors.New("Namespace must not be empty when redeploying kubernetes stacks")}
}
content, err := ioutil.ReadFile(filepath.Join(stack.ProjectPath, stack.GitConfig.ConfigFilePath))
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to read deployment.yml manifest file", Err: errors.Wrap(err, "failed to read manifest file")}
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Failed to retrieve user token data", Err: err}
}
_, err = handler.deployKubernetesStack(r, endpoint, string(content), stack.IsComposeFormat, stack.Namespace, k.KubeAppLabels{
StackID: int(stack.ID),
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "git",
_, err = handler.deployKubernetesStack(tokenData.ID, endpoint, stack, k.KubeAppLabels{
StackID: int(stack.ID),
StackName: stack.Name,
Owner: tokenData.Username,
Kind: "git",
})
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to redeploy Kubernetes stack", Err: errors.WithMessage(err, "failed to deploy kube application")}

View File

@@ -2,7 +2,10 @@ package stacks
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strconv"
"github.com/asaskevich/govalidator"
@@ -10,7 +13,9 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
k "github.com/portainer/portainer/api/kubernetes"
)
@@ -23,6 +28,7 @@ type kubernetesGitStackUpdatePayload struct {
RepositoryAuthentication bool
RepositoryUsername string
RepositoryPassword string
AutoUpdate *portainer.StackAutoUpdate
}
func (payload *kubernetesFileStackUpdatePayload) Validate(r *http.Request) error {
@@ -36,12 +42,20 @@ func (payload *kubernetesGitStackUpdatePayload) Validate(r *http.Request) error
if govalidator.IsNull(payload.RepositoryReferenceName) {
payload.RepositoryReferenceName = defaultGitReferenceName
}
if err := validateStackAutoUpdate(payload.AutoUpdate); err != nil {
return err
}
return nil
}
func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) *httperror.HandlerError {
if stack.GitConfig != nil {
//stop the autoupdate job if there is any
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
}
var payload kubernetesGitStackUpdatePayload
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
@@ -49,6 +63,8 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
}
stack.GitConfig.ReferenceName = payload.RepositoryReferenceName
stack.AutoUpdate = payload.AutoUpdate
if payload.RepositoryAuthentication {
password := payload.RepositoryPassword
if password == "" && stack.GitConfig != nil && stack.GitConfig.Authentication != nil {
@@ -61,6 +77,15 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
} else {
stack.GitConfig.Authentication = nil
}
if payload.AutoUpdate != nil && payload.AutoUpdate.Interval != "" {
jobID, e := startAutoupdate(stack.ID, stack.AutoUpdate.Interval, handler.Scheduler, handler.StackDeployer, handler.DataStore, handler.GitService)
if e != nil {
return e
}
stack.AutoUpdate.JobID = jobID
}
return nil
}
@@ -71,11 +96,27 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
_, err = handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, stack.IsComposeFormat, stack.Namespace, k.KubeAppLabels{
StackID: int(stack.ID),
Name: stack.Name,
Owner: stack.CreatedBy,
Kind: "content",
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Failed to retrieve user token data", Err: err}
}
tempFileDir, _ := ioutil.TempDir("", "kub_file_content")
defer os.RemoveAll(tempFileDir)
if err := filesystem.WriteToFile(path.Join(tempFileDir, stack.EntryPoint), []byte(payload.StackFileContent)); err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to persist deployment file in a temp directory", Err: err}
}
//use temp dir as the stack project path for deployment
//so if the deployment failed, the original file won't be over-written
stack.ProjectPath = tempFileDir
_, err = handler.deployKubernetesStack(tokenData.ID, endpoint, stack, k.KubeAppLabels{
StackID: int(stack.ID),
StackName: stack.Name,
Owner: stack.CreatedBy,
Kind: "content",
})
if err != nil {
@@ -83,7 +124,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
}
stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil {
fileType := "Manifest"
if stack.IsComposeFormat {
@@ -92,6 +133,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
errMsg := fmt.Sprintf("Unable to persist Kubernetes %s file on disk", fileType)
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: errMsg, Err: err}
}
stack.ProjectPath = projectPath
return nil
}

View File

@@ -1,10 +1,10 @@
package stacks
import (
"log"
"net/http"
"github.com/gofrs/uuid"
"github.com/sirupsen/logrus"
"github.com/portainer/libhttp/response"
@@ -31,7 +31,10 @@ func (handler *Handler) webhookInvoke(w http.ResponseWriter, r *http.Request) *h
}
if err = stacks.RedeployWhenChanged(stack.ID, handler.StackDeployer, handler.DataStore, handler.GitService); err != nil {
log.Printf("[ERROR] %s\n", err)
if _, ok := err.(*stacks.StackAuthorMissingErr); ok {
return &httperror.HandlerError{StatusCode: http.StatusConflict, Message: "Autoupdate for the stack isn't available", Err: err}
}
logrus.WithError(err).Error("failed to update the stack")
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to update the stack", Err: err}
}

View File

@@ -6,15 +6,13 @@ import (
"testing"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/bolttest"
"github.com/portainer/portainer/api/bolt"
"github.com/stretchr/testify/assert"
)
func TestHandler_webhookInvoke(t *testing.T) {
store, teardown := bolttest.MustNewTestStore(true)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
webhookID := newGuidString(t)

View File

@@ -15,6 +15,7 @@ import (
// @id UserInspect
// @summary Inspect a user
// @description Retrieve details about a user.
// @description User passwords are filtered out, and should never be accessible.
// @description **Access policy**: administrator
// @tags users
// @security jwt

View File

@@ -12,6 +12,7 @@ import (
// @summary List users
// @description List Portainer users.
// @description Non-administrator users will only be able to list other non-administrator user accounts.
// @description User passwords are filtered out, and should never be accessible.
// @description **Access policy**: restricted
// @tags users
// @security jwt

View File

@@ -35,6 +35,9 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r
}
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
handler.ReverseTunnelService.KeepTunnelAlive(params.endpoint.ID, r.Context(), portainer.WebSocketKeepAlive)
proxy.ServeHTTP(w, r)
return 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 {
@@ -45,7 +53,12 @@ func (handler *Handler) websocketShellPodExec(w http.ResponseWriter, r *http.Req
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find serviceaccount associated with user", err}
}
shellPod, err := cli.CreateUserShellPod(r.Context(), serviceAccount.Name)
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable read settings", err}
}
shellPod, err := cli.CreateUserShellPod(r.Context(), serviceAccount.Name, settings.KubectlShellImage)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create user shell", err}
}

View File

@@ -6,29 +6,37 @@ import (
"net"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/http/proxy/factory/dockercompose"
"github.com/portainer/portainer/api/http/proxy/factory/agent"
"github.com/portainer/portainer/api/internal/endpointutils"
)
// ProxyServer provide an extedned proxy with a local server to forward requests
// ProxyServer provide an extended proxy with a local server to forward requests
type ProxyServer struct {
server *http.Server
Port int
}
func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
// NewAgentProxy creates a new instance of ProxyServer that wrap http requests with agent headers
func (factory *ProxyFactory) NewAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
urlString := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
return &ProxyServer{
Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port,
}, nil
if endpointutils.IsEdgeEndpoint((endpoint)) {
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return nil, errors.Wrap(err, "failed starting tunnel")
}
urlString = fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
}
endpointURL, err := url.Parse(endpoint.URL)
endpointURL, err := parseURL(urlString)
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "failed parsing url %s", endpoint.URL)
}
endpointURL.Scheme = "http"
@@ -37,7 +45,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
if err != nil {
return nil, err
return nil, errors.WithMessage(err, "failed generating tls configuration")
}
httpTransport.TLSClientConfig = config
@@ -46,7 +54,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
proxy.Transport = agent.NewTransport(factory.signatureService, httpTransport)
proxyServer := &ProxyServer{
server: &http.Server{
@@ -57,7 +65,7 @@ func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endp
err = proxyServer.start()
if err != nil {
return nil, err
return nil, errors.Wrap(err, "failed starting proxy server")
}
return proxyServer, nil
@@ -91,3 +99,15 @@ func (proxy *ProxyServer) Close() {
proxy.server.Close()
}
}
// parseURL parses the endpointURL using url.Parse.
//
// to prevent an error when url has port but no protocol prefix
// we add `//` prefix if needed
func parseURL(endpointURL string) (*url.URL, error) {
if !strings.HasPrefix(endpointURL, "http") && !strings.HasPrefix(endpointURL, "tcp") && !strings.HasPrefix(endpointURL, "//") {
endpointURL = fmt.Sprintf("//%s", endpointURL)
}
return url.Parse(endpointURL)
}

View File

@@ -1,4 +1,4 @@
package dockercompose
package agent
import (
"net/http"
@@ -7,17 +7,17 @@ import (
)
type (
// AgentTransport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
AgentTransport struct {
// Transport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
Transport struct {
httpTransport *http.Transport
signatureService portainer.DigitalSignatureService
endpointIdentifier portainer.EndpointID
}
)
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *AgentTransport {
transport := &AgentTransport{
// NewTransport returns a new transport that can be used to send signed requests to a Portainer agent
func NewTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *Transport {
transport := &Transport{
httpTransport: httpTransport,
signatureService: signatureService,
}
@@ -26,8 +26,7 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpT
}
// RoundTrip is the implementation of the the http.RoundTripper interface
func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
if err != nil {
return nil, err

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

@@ -1,170 +0,0 @@
package kubernetes
import (
"net/http"
"path"
"github.com/portainer/portainer/api/http/proxy/factory/utils"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/kubernetes/privateregistries"
v1 "k8s.io/api/core/v1"
)
func (transport *baseTransport) proxySecretRequest(request *http.Request, namespace, requestPath string) (*http.Response, error) {
switch request.Method {
case "POST":
return transport.proxySecretCreationOperation(request)
case "GET":
if path.Base(requestPath) == "secrets" {
return transport.proxySecretListOperation(request)
}
return transport.proxySecretInspectOperation(request)
case "PUT":
return transport.proxySecretUpdateOperation(request)
case "DELETE":
return transport.proxySecretDeleteOperation(request, namespace)
default:
return transport.executeKubernetesRequest(request)
}
}
func (transport *baseTransport) proxySecretCreationOperation(request *http.Request) (*http.Response, error) {
body, err := utils.GetRequestAsMap(request)
if err != nil {
return nil, err
}
if isSecretRepresentPrivateRegistry(body) {
return utils.WriteAccessDeniedResponse()
}
err = utils.RewriteRequest(request, body)
if err != nil {
return nil, err
}
return transport.executeKubernetesRequest(request)
}
func (transport *baseTransport) proxySecretListOperation(request *http.Request) (*http.Response, error) {
response, err := transport.executeKubernetesRequest(request)
if err != nil {
return nil, err
}
isAdmin, err := security.IsAdmin(request)
if err != nil {
return nil, err
}
if isAdmin {
return response, nil
}
body, err := utils.GetResponseAsJSONObject(response)
if err != nil {
return nil, err
}
items := utils.GetArrayObject(body, "items")
if items == nil {
utils.RewriteResponse(response, body, response.StatusCode)
return response, nil
}
filteredItems := []interface{}{}
for _, item := range items {
itemObj := item.(map[string]interface{})
if !isSecretRepresentPrivateRegistry(itemObj) {
filteredItems = append(filteredItems, item)
}
}
body["items"] = filteredItems
utils.RewriteResponse(response, body, response.StatusCode)
return response, nil
}
func (transport *baseTransport) proxySecretInspectOperation(request *http.Request) (*http.Response, error) {
response, err := transport.executeKubernetesRequest(request)
if err != nil {
return nil, err
}
isAdmin, err := security.IsAdmin(request)
if err != nil {
return nil, err
}
if isAdmin {
return response, nil
}
body, err := utils.GetResponseAsJSONObject(response)
if err != nil {
return nil, err
}
if isSecretRepresentPrivateRegistry(body) {
return utils.WriteAccessDeniedResponse()
}
err = utils.RewriteResponse(response, body, response.StatusCode)
if err != nil {
return nil, err
}
return response, nil
}
func (transport *baseTransport) proxySecretUpdateOperation(request *http.Request) (*http.Response, error) {
body, err := utils.GetRequestAsMap(request)
if err != nil {
return nil, err
}
if isSecretRepresentPrivateRegistry(body) {
return utils.WriteAccessDeniedResponse()
}
err = utils.RewriteRequest(request, body)
if err != nil {
return nil, err
}
return transport.executeKubernetesRequest(request)
}
func (transport *baseTransport) proxySecretDeleteOperation(request *http.Request, namespace string) (*http.Response, error) {
kcl, err := transport.k8sClientFactory.GetKubeClient(transport.endpoint)
if err != nil {
return nil, err
}
secretName := path.Base(request.RequestURI)
isRegistry, err := kcl.IsRegistrySecret(namespace, secretName)
if err != nil {
return nil, err
}
if isRegistry {
return utils.WriteAccessDeniedResponse()
}
return transport.executeKubernetesRequest(request)
}
func isSecretRepresentPrivateRegistry(secret map[string]interface{}) bool {
if secret["type"] == nil || secret["type"].(string) != string(v1.SecretTypeDockerConfigJson) {
return false
}
metadata := utils.GetJSONObject(secret, "metadata")
annotations := utils.GetJSONObject(metadata, "annotations")
_, ok := annotations[privateregistries.RegistryIDLabel]
return ok
}

View File

@@ -66,8 +66,6 @@ func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fu
}
switch {
case strings.HasPrefix(requestPath, "secrets"):
return transport.proxySecretRequest(request, namespace, requestPath)
case requestPath == "" && request.Method == "DELETE":
return transport.proxyNamespaceDeleteOperation(request, namespace)
default:
@@ -79,6 +77,18 @@ func (transport *baseTransport) executeKubernetesRequest(request *http.Request)
resp, err := transport.httpTransport.RoundTrip(request)
// This fix was made to resolve a k8s e2e test, more detailed investigation should be done later.
if err == nil && resp.StatusCode == http.StatusMovedPermanently {
oldLocation := resp.Header.Get("Location")
if oldLocation != "" {
stripedPrefix := strings.TrimSuffix(request.RequestURI, request.URL.Path)
// local proxy strips "/kubernetes" but agent proxy and edge agent proxy do not
stripedPrefix = strings.TrimSuffix(stripedPrefix, "/kubernetes")
newLocation := stripedPrefix + "/kubernetes" + oldLocation
resp.Header.Set("Location", newLocation)
}
}
return resp, err
}

View File

@@ -48,10 +48,10 @@ func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpo
return proxy, nil
}
// CreateComposeProxyServer creates a new HTTP reverse proxy based on environment(endpoint) properties and and adds it to the registered proxies.
// CreateAgentProxyServer creates a new HTTP reverse proxy based on environment(endpoint) properties and and adds it to the registered proxies.
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
func (manager *Manager) CreateComposeProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
return manager.proxyFactory.NewDockerComposeAgentProxy(endpoint)
func (manager *Manager) CreateAgentProxyServer(endpoint *portainer.Endpoint) (*factory.ProxyServer, error) {
return manager.proxyFactory.NewAgentProxy(endpoint)
}
// GetEndpointProxy returns the proxy associated to a key

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"
@@ -171,10 +172,15 @@ func (server *Server) Start() error {
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.HelmPackageManager, server.KubeConfigService)
var endpointHelmHandler = helm.NewHandler(requestBouncer, server.DataStore, server.KubernetesDeployer, server.HelmPackageManager, server.KubeConfigService)
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

@@ -11,7 +11,7 @@ func IsLocalEndpoint(endpoint *portainer.Endpoint) bool {
return strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") || endpoint.Type == 5
}
// IsKubernetesEndpoint returns true if this is a kubernetes environment(endpoint)
// IsKubernetesEndpoint returns true if this is a kubernetes endpoint
func IsKubernetesEndpoint(endpoint *portainer.Endpoint) bool {
return endpoint.Type == portainer.KubernetesLocalEnvironment ||
endpoint.Type == portainer.AgentOnKubernetesEnvironment ||
@@ -24,3 +24,8 @@ func IsDockerEndpoint(endpoint *portainer.Endpoint) bool {
endpoint.Type == portainer.AgentOnDockerEnvironment ||
endpoint.Type == portainer.EdgeAgentOnDockerEnvironment
}
// IsEdgeEndpoint returns true if this is an Edge endpoint
func IsEdgeEndpoint(endpoint *portainer.Endpoint) bool {
return endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment
}

View File

@@ -2,9 +2,13 @@ package stackutils
import (
"fmt"
"io/ioutil"
"path"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
k "github.com/portainer/portainer/api/kubernetes"
)
// ResourceControlID returns the stack resource control id
@@ -20,3 +24,39 @@ func GetStackFilePaths(stack *portainer.Stack) []string {
}
return filePaths
}
// CreateTempK8SDeploymentFiles reads manifest files from original stack project path
// then add app labels into the file contents and create temp files for deployment
// return temp file paths and temp dir
func CreateTempK8SDeploymentFiles(stack *portainer.Stack, kubeDeployer portainer.KubernetesDeployer, appLabels k.KubeAppLabels) ([]string, string, error) {
fileNames := append([]string{stack.EntryPoint}, stack.AdditionalFiles...)
var manifestFilePaths []string
tmpDir, err := ioutil.TempDir("", "kub_deployment")
if err != nil {
return nil, "", errors.Wrap(err, "failed to create temp kub deployment directory")
}
for _, fileName := range fileNames {
manifestFilePath := path.Join(tmpDir, fileName)
manifestContent, err := ioutil.ReadFile(path.Join(stack.ProjectPath, fileName))
if err != nil {
return nil, "", errors.Wrap(err, "failed to read manifest file")
}
if stack.IsComposeFormat {
manifestContent, err = kubeDeployer.ConvertCompose(manifestContent)
if err != nil {
return nil, "", errors.Wrap(err, "failed to convert docker compose file to a kube manifest")
}
}
manifestContent, err = k.AddAppLabels(manifestContent, appLabels.ToMap())
if err != nil {
return nil, "", errors.Wrap(err, "failed to add application labels")
}
err = filesystem.WriteToFile(manifestFilePath, []byte(manifestContent))
if err != nil {
return nil, "", errors.Wrap(err, "failed to create temp manifest file")
}
manifestFilePaths = append(manifestFilePaths, manifestFilePath)
}
return manifestFilePaths, tmpDir, nil
}

View File

@@ -38,7 +38,7 @@ func (d *datastore) Close() error { retur
func (d *datastore) CheckCurrentEdition() error { return nil }
func (d *datastore) IsNew() bool { return false }
func (d *datastore) MigrateData(force bool) error { return nil }
func (d *datastore) RollbackToCE() error { return nil }
func (d *datastore) Rollback(force bool) error { return nil }
func (d *datastore) CustomTemplate() portainer.CustomTemplateService { return d.customTemplate }
func (d *datastore) EdgeGroup() portainer.EdgeGroupService { return d.edgeGroup }
func (d *datastore) EdgeJob() portainer.EdgeJobService { return d.edgeJob }

View File

@@ -1,14 +1,13 @@
package cli
import (
"errors"
"fmt"
"net/http"
"strconv"
"sync"
"time"
cmap "github.com/orcaman/concurrent-map"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"k8s.io/client-go/kubernetes"
@@ -116,36 +115,18 @@ func (rt *agentHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response,
func (factory *ClientFactory) buildAgentClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
endpointURL := fmt.Sprintf("https://%s/kubernetes", endpoint.URL)
return factory.createRemoteClient(endpointURL);
return factory.createRemoteClient(endpointURL)
}
func (factory *ClientFactory) buildEdgeClient(endpoint *portainer.Endpoint) (*kubernetes.Clientset, error) {
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
err := factory.reverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return nil, fmt.Errorf("failed opening tunnel to environment: %w", err)
}
if endpoint.EdgeCheckinInterval == 0 {
settings, err := factory.dataStore.Settings().Settings()
if err != nil {
return nil, fmt.Errorf("failed fetching settings from db: %w", err)
}
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
tunnel = factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
tunnel, err := factory.reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return nil, errors.Wrap(err, "failed activating tunnel")
}
endpointURL := fmt.Sprintf("http://127.0.0.1:%d/kubernetes", tunnel.Port)
return factory.createRemoteClient(endpointURL);
return factory.createRemoteClient(endpointURL)
}
func (factory *ClientFactory) createRemoteClient(endpointURL string) (*kubernetes.Clientset, error) {

View File

@@ -12,18 +12,14 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const shellPodImage = "portainer/kubectl-shell"
// CreateUserShellPod will create a kubectl based shell for the specified user by mounting their respective service account.
// The lifecycle of the pod is managed in this function; this entails management of the following pod operations:
// - The shell pod will be scoped to specified service accounts access permissions
// - The shell pod will be automatically removed if it's not ready after specified period of time
// - The shell pod will be automatically removed after a specified max life (prevent zombie pods)
// - The shell pod will be automatically removed if request is cancelled (or client closes websocket connection)
func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountName string) (*portainer.KubernetesShellPod, error) {
// Schedule the pod for automatic removal
maxPodKeepAlive := 1 * time.Hour
maxPodKeepAliveSecondsStr := fmt.Sprintf("%d", int(maxPodKeepAlive.Seconds()))
func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*portainer.KubernetesShellPod, error) {
maxPodKeepAliveSecondsStr := fmt.Sprintf("%d", int(portainer.WebSocketKeepAlive.Seconds()))
podPrefix := userShellPodPrefix(serviceAccountName)
@@ -81,7 +77,7 @@ func (kcl *KubeClient) CreateUserShellPod(ctx context.Context, serviceAccountNam
// Handle pod lifecycle/cleanup - terminate pod after maxPodKeepAlive or upon request (long-lived) cancellation
go func() {
select {
case <-time.After(maxPodKeepAlive):
case <-time.After(portainer.WebSocketKeepAlive):
log.Println("[DEBUG] [internal,kubernetes/pod] [message: pod removal schedule duration exceeded]")
kcl.cli.CoreV1().Pods(portainerNamespace).Delete(shellPod.Name, nil)
case <-ctx.Done():

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

@@ -11,21 +11,66 @@ import (
"gopkg.in/yaml.v3"
)
const (
labelPortainerAppStack = "io.portainer.kubernetes.application.stack"
labelPortainerAppStackID = "io.portainer.kubernetes.application.stackid"
labelPortainerAppName = "io.portainer.kubernetes.application.name"
labelPortainerAppOwner = "io.portainer.kubernetes.application.owner"
labelPortainerAppKind = "io.portainer.kubernetes.application.kind"
)
// KubeAppLabels are labels applied to all resources deployed in a kubernetes stack
type KubeAppLabels struct {
StackID int
Name string
Owner string
Kind string
StackID int
StackName string
Owner string
Kind string
}
// ToMap converts KubeAppLabels to a map[string]string
func (kal *KubeAppLabels) ToMap() map[string]string {
return map[string]string{
labelPortainerAppStackID: strconv.Itoa(kal.StackID),
labelPortainerAppStack: kal.StackName,
labelPortainerAppName: kal.StackName,
labelPortainerAppOwner: kal.Owner,
labelPortainerAppKind: kal.Kind,
}
}
// GetHelmAppLabels returns the labels to be applied to portainer deployed helm applications
func GetHelmAppLabels(name, owner string) map[string]string {
return map[string]string{
labelPortainerAppName: name,
labelPortainerAppOwner: owner,
}
}
// AddAppLabels adds required labels to "Resource"->metadata->labels.
// It'll add those labels to all Resource (nodes with a kind property exluding a list) it can find in provided yaml.
// Items in the yaml file could either be organised as a list or broken into multi documents.
func AddAppLabels(manifestYaml []byte, appLabels KubeAppLabels) ([]byte, error) {
func AddAppLabels(manifestYaml []byte, appLabels map[string]string) ([]byte, error) {
if bytes.Equal(manifestYaml, []byte("")) {
return manifestYaml, nil
}
postProcessYaml := func(yamlDoc interface{}) error {
addResourceLabels(yamlDoc, appLabels)
return nil
}
docs, err := ExtractDocuments(manifestYaml, postProcessYaml)
if err != nil {
return nil, err
}
return bytes.Join(docs, []byte("---\n")), nil
}
// ExtractDocuments extracts all the documents from a yaml file
// Optionally post-process each document with a function, which can modify the document in place.
// Pass in nil for postProcessYaml to skip post-processing.
func ExtractDocuments(manifestYaml []byte, postProcessYaml func(interface{}) error) ([][]byte, error) {
docs := make([][]byte, 0)
yamlDecoder := yaml.NewDecoder(bytes.NewReader(manifestYaml))
@@ -43,7 +88,12 @@ func AddAppLabels(manifestYaml []byte, appLabels KubeAppLabels) ([]byte, error)
break
}
addResourceLabels(m, appLabels)
// optionally post-process yaml
if postProcessYaml != nil {
if err := postProcessYaml(m); err != nil {
return nil, errors.Wrap(err, "failed to post process yaml document")
}
}
var out bytes.Buffer
yamlEncoder := yaml.NewEncoder(&out)
@@ -55,10 +105,29 @@ func AddAppLabels(manifestYaml []byte, appLabels KubeAppLabels) ([]byte, error)
docs = append(docs, out.Bytes())
}
return bytes.Join(docs, []byte("---\n")), nil
return docs, nil
}
func addResourceLabels(yamlDoc interface{}, appLabels KubeAppLabels) {
// GetNamespace returns the namespace of a kubernetes resource from its metadata
// It returns an empty string if namespace is not found in the resource
func GetNamespace(manifestYaml []byte) (string, error) {
yamlDecoder := yaml.NewDecoder(bytes.NewReader(manifestYaml))
m := make(map[string]interface{})
err := yamlDecoder.Decode(&m)
if err != nil {
return "", errors.Wrap(err, "failed to unmarshal yaml manifest when obtaining namespace")
}
if _, ok := m["metadata"]; ok {
if namespace, ok := m["metadata"].(map[string]interface{})["namespace"]; ok {
return namespace.(string), nil
}
}
return "", nil
}
func addResourceLabels(yamlDoc interface{}, appLabels map[string]string) {
m, ok := yamlDoc.(map[string]interface{})
if !ok {
return
@@ -82,7 +151,7 @@ func addResourceLabels(yamlDoc interface{}, appLabels KubeAppLabels) {
}
}
func addLabels(obj map[string]interface{}, appLabels KubeAppLabels) {
func addLabels(obj map[string]interface{}, appLabels map[string]string) {
metadata := make(map[string]interface{})
if m, ok := obj["metadata"]; ok {
metadata = m.(map[string]interface{})
@@ -95,18 +164,11 @@ func addLabels(obj map[string]interface{}, appLabels KubeAppLabels) {
}
}
name := appLabels.Name
if appLabels.Name == "" {
if n, ok := metadata["name"]; ok {
name = n.(string)
}
// merge app labels with existing labels
for k, v := range appLabels {
labels[k] = v
}
labels["io.portainer.kubernetes.application.stackid"] = strconv.Itoa(appLabels.StackID)
labels["io.portainer.kubernetes.application.name"] = name
labels["io.portainer.kubernetes.application.owner"] = appLabels.Owner
labels["io.portainer.kubernetes.application.kind"] = appLabels.Kind
metadata["labels"] = labels
obj["metadata"] = metadata
}

View File

@@ -39,6 +39,7 @@ metadata:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: busybox
spec:
@@ -86,6 +87,7 @@ metadata:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: busybox
spec:
@@ -174,6 +176,7 @@ items:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
@@ -194,6 +197,7 @@ items:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: redis
spec:
@@ -216,6 +220,7 @@ items:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
@@ -297,6 +302,7 @@ metadata:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: busybox
spec:
@@ -322,6 +328,7 @@ metadata:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
@@ -340,6 +347,7 @@ metadata:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: busybox
spec:
@@ -388,6 +396,7 @@ metadata:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
@@ -402,10 +411,174 @@ spec:
}
labels := KubeAppLabels{
StackID: 123,
Name: "best-name",
Owner: "best-owner",
Kind: "git",
StackID: 123,
StackName: "best-name",
Owner: "best-owner",
Kind: "git",
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := AddAppLabels([]byte(tt.input), labels.ToMap())
assert.NoError(t, err)
assert.Equal(t, tt.wantOutput, string(result))
})
}
}
func Test_AddAppLabels_HelmApp(t *testing.T) {
labels := GetHelmAppLabels("best-name", "best-owner")
tests := []struct {
name string
input string
wantOutput string
}{
{
name: "bitnami nginx configmap",
input: `apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-test-server-block
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-9.5.4
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/managed-by: Helm
data:
server-blocks-paths.conf: |-
include "/opt/bitnami/nginx/conf/server_blocks/ldap/*.conf";
include "/opt/bitnami/nginx/conf/server_blocks/common/*.conf";
`,
wantOutput: `apiVersion: v1
data:
server-blocks-paths.conf: |-
include "/opt/bitnami/nginx/conf/server_blocks/ldap/*.conf";
include "/opt/bitnami/nginx/conf/server_blocks/common/*.conf";
kind: ConfigMap
metadata:
labels:
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-9.5.4
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
name: nginx-test-server-block
`,
},
{
name: "bitnami nginx service",
input: `apiVersion: v1
kind: Service
metadata:
name: nginx-test
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-9.5.4
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/managed-by: Helm
spec:
type: LoadBalancer
externalTrafficPolicy: "Cluster"
ports:
- name: http
port: 80
targetPort: http
selector:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: nginx-test
`,
wantOutput: `apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-9.5.4
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
name: nginx-test
spec:
externalTrafficPolicy: Cluster
ports:
- name: http
port: 80
targetPort: http
selector:
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/name: nginx
type: LoadBalancer
`,
},
{
name: "bitnami nginx deployment",
input: `apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-9.5.4
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/managed-by: Helm
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: nginx-test
template:
metadata:
labels:
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-9.5.4
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/managed-by: Helm
spec:
automountServiceAccountToken: false
shareProcessNamespace: false
serviceAccountName: default
containers:
- name: nginx
image: docker.io/bitnami/nginx:1.21.3-debian-10-r0
imagePullPolicy: "IfNotPresent"
`,
wantOutput: `apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-9.5.4
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
name: nginx-test
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/name: nginx
template:
metadata:
labels:
app.kubernetes.io/instance: nginx-test
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: nginx
helm.sh/chart: nginx-9.5.4
spec:
automountServiceAccountToken: false
containers:
- image: docker.io/bitnami/nginx:1.21.3-debian-10-r0
imagePullPolicy: IfNotPresent
name: nginx
serviceAccountName: default
shareProcessNamespace: false
`,
},
}
for _, tt := range tests {
@@ -417,77 +590,124 @@ spec:
}
}
func Test_AddAppLabels_PickingName_WhenLabelNameIsEmpty(t *testing.T) {
func Test_DocumentSeperator(t *testing.T) {
labels := KubeAppLabels{
StackID: 123,
Owner: "best-owner",
Kind: "git",
StackID: 123,
StackName: "best-name",
Owner: "best-owner",
Kind: "git",
}
input := `apiVersion: v1
kind: Service
metadata:
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
labels:
io.kompose.service: database
---
apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: backend
`
expected := `apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: database
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: web
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
---
apiVersion: v1
kind: Service
metadata:
labels:
io.kompose.service: backend
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: best-name
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stack: best-name
io.portainer.kubernetes.application.stackid: "123"
name: web
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
`
result, err := AddAppLabels([]byte(input), labels)
result, err := AddAppLabels([]byte(input), labels.ToMap())
assert.NoError(t, err)
assert.Equal(t, expected, string(result))
}
func Test_AddAppLabels_PickingName_WhenLabelAndMetadataNameAreEmpty(t *testing.T) {
labels := KubeAppLabels{
StackID: 123,
Owner: "best-owner",
Kind: "git",
func Test_GetNamespace(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{
name: "valid namespace",
input: `apiVersion: v1
kind: Namespace
metadata:
namespace: test-namespace
`,
want: "test-namespace",
},
{
name: "invalid namespace",
input: `apiVersion: v1
kind: Namespace
`,
want: "",
},
}
input := `apiVersion: v1
kind: Service
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
`
expected := `apiVersion: v1
kind: Service
metadata:
labels:
io.portainer.kubernetes.application.kind: git
io.portainer.kubernetes.application.name: ""
io.portainer.kubernetes.application.owner: best-owner
io.portainer.kubernetes.application.stackid: "123"
spec:
ports:
- name: "5000"
port: 5000
targetPort: 5000
`
result, err := AddAppLabels([]byte(input), labels)
assert.NoError(t, err)
assert.Equal(t, expected, string(result))
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := GetNamespace([]byte(tt.input))
assert.NoError(t, err)
assert.Equal(t, tt.want, result)
})
}
}
func Test_ExtractDocuments(t *testing.T) {
tests := []struct {
name string
input string
want []string
}{
{
name: "multiple documents",
input: `apiVersion: v1
kind: Namespace
---
apiVersion: v1
kind: Service
`,
want: []string{`apiVersion: v1
kind: Namespace
`, `apiVersion: v1
kind: Service
`},
},
{
name: "single document",
input: `apiVersion: v1
kind: Namespace
`,
want: []string{`apiVersion: v1
kind: Namespace
`},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results, err := ExtractDocuments([]byte(tt.input), nil)
assert.NoError(t, err)
for i := range results {
assert.Equal(t, tt.want[i], string(results[i]))
}
})
}
}

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

@@ -3,7 +3,6 @@ package portainer
import (
"context"
"io"
"net/http"
"time"
gittypes "github.com/portainer/portainer/api/git/types"
@@ -66,6 +65,7 @@ type (
SSL *bool
SSLCert *string
SSLKey *string
Rollback *bool
SnapshotInterval *string
}
@@ -148,7 +148,7 @@ type (
Name string `json:"Name"`
Dynamic bool `json:"Dynamic"`
TagIDs []TagID `json:"TagIds"`
Endpoints []EndpointID `json:"Environments"`
Endpoints []EndpointID `json:"Endpoints"`
PartialMatch bool `json:"PartialMatch"`
}
@@ -161,7 +161,7 @@ type (
ID EdgeJobID `json:"Id" example:"1"`
Created int64 `json:"Created"`
CronExpression string `json:"CronExpression"`
Endpoints map[EndpointID]EdgeJobEndpointMeta `json:"Environments"`
Endpoints map[EndpointID]EdgeJobEndpointMeta `json:"Endpoints"`
Name string `json:"Name"`
ScriptPath string `json:"ScriptPath"`
Recurring bool `json:"Recurring"`
@@ -188,7 +188,7 @@ type (
CronExpression string `json:"CronExpression"`
Script string `json:"Script"`
Version int `json:"Version"`
Endpoints []EndpointID `json:"Environments"`
Endpoints []EndpointID `json:"Endpoints"`
}
//EdgeStack represents an edge stack
@@ -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"`
@@ -713,6 +719,8 @@ type (
EnableTelemetry bool `json:"EnableTelemetry" example:"false"`
// Helm repository URL, defaults to "https://charts.bitnami.com/bitnami"
HelmRepositoryURL string `json:"HelmRepositoryURL" example:"https://charts.bitnami.com/bitnami"`
// KubectlImage, defaults to portainer/kubectl-shell
KubectlShellImage string `json:"KubectlShellImage" example:"portainer/kubectl-shell"`
// Deprecated fields
DisplayDonationHeader bool
@@ -1022,7 +1030,7 @@ type (
// User Identifier
ID UserID `json:"Id" example:"1"`
Username string `json:"Username" example:"bob"`
Password string `json:"Password,omitempty" example:"passwd"`
Password string `json:"Password,omitempty" swaggerignore:"true"`
// User Theme
UserTheme string `example:"dark"`
// User role (1 for administrator account and 2 for regular account)
@@ -1103,6 +1111,7 @@ type (
Close() error
IsNew() bool
MigrateData(force bool) error
Rollback(force bool) error
CheckCurrentEdition() error
BackupTo(w io.Writer) error
@@ -1204,6 +1213,7 @@ type (
FileService interface {
GetDockerConfigPath() string
GetFileContent(filePath string) ([]byte, error)
Copy(fromFilePath string, toFilePath string, deleteIfExists bool) error
Rename(oldPath, newPath string) error
RemoveDirectory(directoryPath string) error
StoreTLSFileFromBytes(folder string, fileType TLSFileType, data []byte) (string, error)
@@ -1261,7 +1271,7 @@ type (
SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error
GetServiceAccount(tokendata *TokenData) (*v1.ServiceAccount, error)
GetServiceAccountBearerToken(userID int) (string, error)
CreateUserShellPod(ctx context.Context, serviceAccountName string) (*KubernetesShellPod, error)
CreateUserShellPod(ctx context.Context, serviceAccountName, shellPodImage string) (*KubernetesShellPod, error)
StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer, errChan chan error)
NamespaceAccessPoliciesDeleteNamespace(namespace string) error
GetNodesLimits() (K8sNodesLimits, error)
@@ -1276,7 +1286,8 @@ type (
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes environment(endpoint)
KubernetesDeployer interface {
Deploy(request *http.Request, endpoint *Endpoint, data string, namespace string) (string, error)
Deploy(userID UserID, endpoint *Endpoint, manifestFiles []string, namespace string) (string, error)
Remove(userID UserID, endpoint *Endpoint, manifestFiles []string, namespace string) (string, error)
ConvertCompose(data []byte) ([]byte, error)
}
@@ -1290,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
@@ -1324,7 +1337,9 @@ type (
SetTunnelStatusToActive(endpointID EndpointID)
SetTunnelStatusToRequired(endpointID EndpointID) error
SetTunnelStatusToIdle(endpointID EndpointID)
KeepTunnelAlive(endpointID EndpointID, ctx context.Context, maxKeepAlive time.Duration)
GetTunnelDetails(endpointID EndpointID) *TunnelDetails
GetActiveTunnel(endpoint *Endpoint) (*TunnelDetails, error)
AddEdgeJob(endpointID EndpointID, edgeJob *EdgeJob)
RemoveEdgeJob(edgeJobID EdgeJobID)
}
@@ -1455,9 +1470,9 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.9.0"
APIVersion = "2.9.2"
// DBVersion is the version number of the Portainer database
DBVersion = 32
DBVersion = 33
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
ComposeSyntaxMaxVersion = "3.9"
// AssetsServerURL represents the URL of the Portainer asset server
@@ -1493,6 +1508,10 @@ const (
DefaultUserSessionTimeout = "8h"
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared
DefaultKubeconfigExpiry = "0"
// DefaultKubectlShellImage represents the default image and tag for the kubectl shell
DefaultKubectlShellImage = "portainer/kubectl-shell"
// WebSocketKeepAlive web socket keep alive for edge environments
WebSocketKeepAlive = 1 * time.Hour
)
const (

View File

@@ -8,11 +8,12 @@ import (
"github.com/pkg/errors"
"github.com/robfig/cron/v3"
"github.com/sirupsen/logrus"
)
type Scheduler struct {
crontab *cron.Cron
shutdownCtx context.Context
crontab *cron.Cron
activeJobs map[cron.EntryID]context.CancelFunc
}
func NewScheduler(ctx context.Context) *Scheduler {
@@ -20,7 +21,8 @@ func NewScheduler(ctx context.Context) *Scheduler {
crontab.Start()
s := &Scheduler{
crontab: crontab,
crontab: crontab,
activeJobs: make(map[cron.EntryID]context.CancelFunc),
}
if ctx != nil {
@@ -43,8 +45,10 @@ func (s *Scheduler) Shutdown() error {
ctx := s.crontab.Stop()
<-ctx.Done()
for _, j := range s.crontab.Entries() {
s.crontab.Remove(j.ID)
for _, job := range s.crontab.Entries() {
if cancel, ok := s.activeJobs[job.ID]; ok {
cancel()
}
}
err := ctx.Err()
@@ -60,14 +64,36 @@ func (s *Scheduler) StopJob(jobID string) error {
if err != nil {
return errors.Wrapf(err, "failed convert jobID %q to int", jobID)
}
s.crontab.Remove(cron.EntryID(id))
entryID := cron.EntryID(id)
if cancel, ok := s.activeJobs[entryID]; ok {
cancel()
}
return nil
}
// StartJobEvery schedules a new periodic job with a given duration.
// Returns job id that could be used to stop the given job
func (s *Scheduler) StartJobEvery(duration time.Duration, job func()) string {
entryId := s.crontab.Schedule(cron.Every(duration), cron.FuncJob(job))
return strconv.Itoa(int(entryId))
// Returns job id that could be used to stop the given job.
// When job run returns an error, that job won't be run again.
func (s *Scheduler) StartJobEvery(duration time.Duration, job func() error) string {
ctx, cancel := context.WithCancel(context.Background())
j := cron.FuncJob(func() {
if err := job(); err != nil {
logrus.Debug("job returned an error")
cancel()
}
})
entryID := s.crontab.Schedule(cron.Every(duration), j)
s.activeJobs[entryID] = cancel
go func(entryID cron.EntryID) {
<-ctx.Done()
logrus.Debug("job cancelled, stopping")
s.crontab.Remove(entryID)
}(entryID)
return strconv.Itoa(int(entryID))
}

View File

@@ -9,49 +9,92 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_CanStartAndTerminate(t *testing.T) {
s := NewScheduler(context.Background())
s.StartJobEvery(1*time.Minute, func() { fmt.Println("boop") })
var jobInterval = time.Second
err := s.Shutdown()
assert.NoError(t, err, "Shutdown should return no errors")
assert.Empty(t, s.crontab.Entries(), "all jobs should have been removed")
}
func Test_CanTerminateByCancellingContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
s := NewScheduler(ctx)
s.StartJobEvery(1*time.Minute, func() { fmt.Println("boop") })
cancel()
for i := 0; i < 100; i++ {
if len(s.crontab.Entries()) == 0 {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("all jobs are expected to be cleaned by now; it might be a timing issue, otherwise implementation defect")
}
func Test_StartAndStopJob(t *testing.T) {
func Test_ScheduledJobRuns(t *testing.T) {
s := NewScheduler(context.Background())
defer s.Shutdown()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 2*jobInterval)
var jobOne string
var workDone bool
jobOne = s.StartJobEvery(time.Second, func() {
assert.Equal(t, 1, len(s.crontab.Entries()), "scheduler should have one active job")
s.StartJobEvery(jobInterval, func() error {
workDone = true
s.StopJob(jobOne)
cancel()
return nil
})
<-ctx.Done()
assert.True(t, workDone, "value should been set in the job")
assert.Equal(t, 0, len(s.crontab.Entries()), "scheduler should have no active jobs")
}
func Test_JobCanBeStopped(t *testing.T) {
s := NewScheduler(context.Background())
defer s.Shutdown()
ctx, cancel := context.WithTimeout(context.Background(), 2*jobInterval)
var workDone bool
jobID := s.StartJobEvery(jobInterval, func() error {
workDone = true
cancel()
return nil
})
s.StopJob(jobID)
<-ctx.Done()
assert.False(t, workDone, "job shouldn't had a chance to run")
}
func Test_JobShouldStop_UponError(t *testing.T) {
s := NewScheduler(context.Background())
defer s.Shutdown()
var acc int
s.StartJobEvery(jobInterval, func() error {
acc++
return fmt.Errorf("failed")
})
<-time.After(3 * jobInterval)
assert.Equal(t, 1, acc, "job stop after the first run because it returns an error")
}
func Test_CanTerminateAllJobs_ByShuttingDownScheduler(t *testing.T) {
s := NewScheduler(context.Background())
ctx, cancel := context.WithTimeout(context.Background(), 2*jobInterval)
var workDone bool
s.StartJobEvery(jobInterval, func() error {
workDone = true
cancel()
return nil
})
s.Shutdown()
<-ctx.Done()
assert.False(t, workDone, "job shouldn't had a chance to run")
}
func Test_CanTerminateAllJobs_ByCancellingParentContext(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*jobInterval)
s := NewScheduler(ctx)
var workDone bool
s.StartJobEvery(jobInterval, func() error {
workDone = true
cancel()
return nil
})
cancel()
<-ctx.Done()
assert.False(t, workDone, "job shouldn't had a chance to run")
}

View File

@@ -1,15 +1,29 @@
package stacks
import (
"fmt"
"strings"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/security"
log "github.com/sirupsen/logrus"
)
type StackAuthorMissingErr struct {
stackID int
authorName string
}
func (e *StackAuthorMissingErr) Error() string {
return fmt.Sprintf("stack's %v author %s is missing", e.stackID, e.authorName)
}
func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, datastore portainer.DataStore, gitService portainer.GitService) error {
logger := log.WithFields(log.Fields{"stackID": stackID})
logger.Debug("redeploying stack")
stack, err := datastore.Stack().Stack(stackID)
if err != nil {
return errors.WithMessagef(err, "failed to get the stack %v", stackID)
@@ -19,6 +33,17 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
return nil // do nothing if it isn't a git-based stack
}
author := stack.UpdatedBy
if author == "" {
author = stack.CreatedBy
}
user, err := datastore.User().UserByUsername(author)
if err != nil {
logger.WithFields(log.Fields{"author": author, "stack": stack.Name, "endpointID": stack.EndpointID}).Warn("cannot autoupdate a stack, stack author user is missing")
return &StackAuthorMissingErr{int(stack.ID), author}
}
username, password := "", ""
if stack.GitConfig.Authentication != nil {
username, password = stack.GitConfig.Authentication.Username, stack.GitConfig.Authentication.Password
@@ -54,12 +79,7 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
return errors.WithMessagef(err, "failed to find the environment %v associated to the stack %v", stack.EndpointID, stack.ID)
}
author := stack.UpdatedBy
if author == "" {
author = stack.CreatedBy
}
registries, err := getUserRegistries(datastore, author, endpoint.ID)
registries, err := getUserRegistries(datastore, user, endpoint.ID)
if err != nil {
return err
}
@@ -75,6 +95,12 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
if err != nil {
return errors.WithMessagef(err, "failed to deploy a docker compose stack %v", stackID)
}
case portainer.KubernetesStack:
logger.Debugf("deploying a kube app")
err := deployer.DeployKubernetesStack(stack, endpoint, user)
if err != nil {
return errors.WithMessagef(err, "failed to deploy a kubternetes app stack %v", stackID)
}
default:
return errors.Errorf("cannot update stack, type %v is unsupported", stack.Type)
}
@@ -88,24 +114,19 @@ func RedeployWhenChanged(stackID portainer.StackID, deployer StackDeployer, data
return nil
}
func getUserRegistries(datastore portainer.DataStore, authorUsername string, endpointID portainer.EndpointID) ([]portainer.Registry, error) {
func getUserRegistries(datastore portainer.DataStore, user *portainer.User, endpointID portainer.EndpointID) ([]portainer.Registry, error) {
registries, err := datastore.Registry().Registries()
if err != nil {
return nil, errors.WithMessage(err, "unable to retrieve registries from the database")
}
user, err := datastore.User().UserByUsername(authorUsername)
if err != nil {
return nil, errors.WithMessagef(err, "failed to fetch a stack's author [%s]", authorUsername)
}
if user.Role == portainer.AdministratorRole {
return registries, nil
}
userMemberships, err := datastore.TeamMembership().TeamMembershipsByUserID(user.ID)
if err != nil {
return nil, errors.WithMessagef(err, "failed to fetch memberships of the stack author [%s]", authorUsername)
return nil, errors.WithMessagef(err, "failed to fetch memberships of the stack author [%s]", user.Username)
}
filteredRegistries := make([]portainer.Registry, 0, len(registries))

View File

@@ -7,7 +7,7 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
bolt "github.com/portainer/portainer/api/bolt/bolttest"
"github.com/portainer/portainer/api/bolt"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/stretchr/testify/assert"
)
@@ -35,6 +35,10 @@ func (s *noopDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *port
return nil
}
func (s *noopDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
return nil
}
func Test_redeployWhenChanged_FailsWhenCannotFindStack(t *testing.T) {
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
@@ -48,7 +52,11 @@ func Test_redeployWhenChanged_DoesNothingWhenNotAGitBasedStack(t *testing.T) {
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
err := store.Stack().CreateStack(&portainer.Stack{ID: 1})
admin := &portainer.User{ID: 1, Username: "admin"}
err := store.User().CreateUser(admin)
assert.NoError(t, err, "error creating an admin")
err = store.Stack().CreateStack(&portainer.Stack{ID: 1, CreatedBy: "admin"})
assert.NoError(t, err, "failed to create a test stack")
err = RedeployWhenChanged(1, nil, store, &gitService{nil, ""})
@@ -61,8 +69,13 @@ func Test_redeployWhenChanged_DoesNothingWhenNoGitChanges(t *testing.T) {
tmpDir, _ := ioutil.TempDir("", "stack")
err := store.Stack().CreateStack(&portainer.Stack{
admin := &portainer.User{ID: 1, Username: "admin"}
err := store.User().CreateUser(admin)
assert.NoError(t, err, "error creating an admin")
err = store.Stack().CreateStack(&portainer.Stack{
ID: 1,
CreatedBy: "admin",
ProjectPath: tmpDir,
GitConfig: &gittypes.RepoConfig{
URL: "url",
@@ -80,8 +93,13 @@ func Test_redeployWhenChanged_FailsWhenCannotClone(t *testing.T) {
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
err := store.Stack().CreateStack(&portainer.Stack{
ID: 1,
admin := &portainer.User{ID: 1, Username: "admin"}
err := store.User().CreateUser(admin)
assert.NoError(t, err, "error creating an admin")
err = store.Stack().CreateStack(&portainer.Stack{
ID: 1,
CreatedBy: "admin",
GitConfig: &gittypes.RepoConfig{
URL: "url",
ReferenceName: "ref",
@@ -136,12 +154,12 @@ func Test_redeployWhenChanged(t *testing.T) {
assert.NoError(t, err)
})
t.Run("can NOT deploy kube stack", func(t *testing.T) {
t.Run("can deploy kube app", func(t *testing.T) {
stack.Type = portainer.KubernetesStack
store.Stack().UpdateStack(stack.ID, &stack)
err = RedeployWhenChanged(1, &noopDeployer{}, store, &gitService{nil, "newHash"})
assert.EqualError(t, err, "cannot update stack, type 3 is unsupported")
assert.NoError(t, err)
})
}
@@ -151,12 +169,12 @@ func Test_getUserRegistries(t *testing.T) {
endpointID := 123
admin := portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
err := store.User().CreateUser(&admin)
admin := &portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
err := store.User().CreateUser(admin)
assert.NoError(t, err, "error creating an admin")
user := portainer.User{ID: 2, Username: "user", Role: portainer.StandardUserRole}
err = store.User().CreateUser(&user)
user := &portainer.User{ID: 2, Username: "user", Role: portainer.StandardUserRole}
err = store.User().CreateUser(user)
assert.NoError(t, err, "error creating a user")
team := portainer.Team{ID: 1, Name: "team"}
@@ -208,13 +226,13 @@ func Test_getUserRegistries(t *testing.T) {
assert.NoError(t, err, "couldn't create a registry")
t.Run("admin should has access to all registries", func(t *testing.T) {
registries, err := getUserRegistries(store, admin.Username, portainer.EndpointID(endpointID))
registries, err := getUserRegistries(store, admin, portainer.EndpointID(endpointID))
assert.NoError(t, err)
assert.ElementsMatch(t, []portainer.Registry{registryReachableByUser, registryReachableByTeam, registryRestricted}, registries)
})
t.Run("regular user has access to registries allowed to him and/or his team", func(t *testing.T) {
registries, err := getUserRegistries(store, user.Username, portainer.EndpointID(endpointID))
registries, err := getUserRegistries(store, user, portainer.EndpointID(endpointID))
assert.NoError(t, err)
assert.ElementsMatch(t, []portainer.Registry{registryReachableByUser, registryReachableByTeam}, registries)
})

View File

@@ -2,27 +2,36 @@ package stacks
import (
"context"
"os"
"sync"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/stackutils"
k "github.com/portainer/portainer/api/kubernetes"
)
type StackDeployer interface {
DeploySwarmStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry, prune bool) error
DeployComposeStack(stack *portainer.Stack, endpoint *portainer.Endpoint, registries []portainer.Registry) error
DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error
}
type stackDeployer struct {
lock *sync.Mutex
swarmStackManager portainer.SwarmStackManager
composeStackManager portainer.ComposeStackManager
kubernetesDeployer portainer.KubernetesDeployer
}
func NewStackDeployer(swarmStackManager portainer.SwarmStackManager, composeStackManager portainer.ComposeStackManager) *stackDeployer {
// NewStackDeployer inits a stackDeployer struct with a SwarmStackManager, a ComposeStackManager and a KubernetesDeployer
func NewStackDeployer(swarmStackManager portainer.SwarmStackManager, composeStackManager portainer.ComposeStackManager, kubernetesDeployer portainer.KubernetesDeployer) *stackDeployer {
return &stackDeployer{
lock: &sync.Mutex{},
swarmStackManager: swarmStackManager,
composeStackManager: composeStackManager,
kubernetesDeployer: kubernetesDeployer,
}
}
@@ -45,3 +54,33 @@ func (d *stackDeployer) DeployComposeStack(stack *portainer.Stack, endpoint *por
return d.composeStackManager.Up(context.TODO(), stack, endpoint)
}
func (d *stackDeployer) DeployKubernetesStack(stack *portainer.Stack, endpoint *portainer.Endpoint, user *portainer.User) error {
d.lock.Lock()
defer d.lock.Unlock()
appLabels := k.KubeAppLabels{
StackID: int(stack.ID),
StackName: stack.Name,
Owner: user.Username,
}
if stack.GitConfig == nil {
appLabels.Kind = "content"
} else {
appLabels.Kind = "git"
}
manifestFilePaths, tempDir, err := stackutils.CreateTempK8SDeploymentFiles(stack, d.kubernetesDeployer, appLabels)
if err != nil {
return errors.Wrap(err, "failed to create temp kub deployment files")
}
defer os.RemoveAll(tempDir)
_, err = d.kubernetesDeployer.Deploy(user.ID, endpoint, manifestFilePaths, stack.Namespace)
if err != nil {
return errors.Wrap(err, "failed to deploy kubernetes application")
}
return nil
}

View File

@@ -1,7 +1,6 @@
package stacks
import (
"log"
"time"
"github.com/pkg/errors"
@@ -19,10 +18,9 @@ func StartStackSchedules(scheduler *scheduler.Scheduler, stackdeployer StackDepl
if err != nil {
return errors.Wrap(err, "Unable to parse auto update interval")
}
jobID := scheduler.StartJobEvery(d, func() {
if err := RedeployWhenChanged(stack.ID, stackdeployer, datastore, gitService); err != nil {
log.Printf("[ERROR] %s\n", err)
}
stackID := stack.ID // to be captured by the scheduled function
jobID := scheduler.StartJobEvery(d, func() error {
return RedeployWhenChanged(stackID, stackdeployer, datastore, gitService)
})
stack.AutoUpdate.JobID = jobID

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

@@ -88,8 +88,14 @@ 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);
@@ -150,6 +156,8 @@ html {
--bg-btn-focus: var(--grey-59);
--bg-boxselector-disabled-color: var(--white-color);
--bg-small-select-color: var(--white-color);
--bg-app-datatable-thead: var(--grey-23);
--bg-app-datatable-tbody: var(--grey-24);
--text-main-color: var(--grey-7);
--text-body-color: var(--grey-6);
@@ -318,6 +326,8 @@ html {
--bg-btn-focus: var(--grey-3);
--bg-boxselector-disabled-color: var(--grey-54);
--bg-small-select-color: var(--grey-2);
--bg-app-datatable-thead: var(--grey-1);
--bg-app-datatable-tbody: var(--grey-1);
--text-main-color: var(--white-color);
--text-body-color: var(--white-color);
@@ -485,6 +495,8 @@ html {
--bg-boxselector-color: var(--black-color);
--bg-boxselector-disabled-color: var(--black-color);
--bg-small-select-color: var(--black-color);
--bg-app-datatable-thead: var(--black-color);
--bg-app-datatable-tbody: var(--black-color);
--text-main-color: var(--white-color);
--text-body-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);
}
@@ -287,7 +281,7 @@ json-tree .branch-preview {
margin-top: 15px;
}
.summary {
.bold {
color: var(--text-summary-color);
font-weight: 700;
}
@@ -396,4 +390,19 @@ input:-webkit-autofill {
.btn-primary:hover {
color: var(--white-color) !important;
}
.btn-danger:hover {
color: var(--white-color);
}
/* Overide Vendor CSS */
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn {
pointer-events: none;
touch-action: none;
}
.multiSelect.inlineBlock button {
margin: 0;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 B

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 940 300" style="enable-background:new 0 0 940 300;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#13BEF9;}
.st1{fill:#13BEF9;}
</style>
<g>
<polygon class="st0" points="84.3,76.6 80.3,76.6 80.3,97.3 84.3,97.3 84.3,76.6 "/>
<polygon class="st0" points="101.5,76.6 97.5,76.6 97.5,97.3 101.5,97.3 101.5,76.6 "/>
<polygon class="st0" points="125,37.1 120.9,30 52.5,69.5 56.6,76.6 125,37.1 "/>
<polygon class="st0" points="124.6,37.1 128.7,30 197.1,69.5 193,76.6 124.6,37.1 "/>
<polygon class="st0" points="209.2,76.7 209.2,68.5 21.8,68.5 21.8,76.7 209.2,76.7 "/>
<path class="st0" d="M135,192.5V71h8.2v127.4C141,195.9,138.2,194.1,135,192.5L135,192.5z"/>
<path class="st0" d="M121,190.4V19h8.2v172.4C126.9,190.3,121.3,190.4,121,190.4L121,190.4z"/>
<path class="st0" d="M43.3,207.5c-10-7.4-16.6-19.2-16.6-32.6c0-7.1,1.9-14.1,5.4-20.2h70c3.6,6.1,5.4,13.1,5.4,20.2
c0,6.2-0.8,12-3.3,17.2c-5.3-5.1-13.1-7.3-21-7.3c-14,0-26,8.7-29.1,21.7c-1.1-0.1-1.8-0.2-2.9-0.2
C48.5,206.4,45.9,206.8,43.3,207.5L43.3,207.5z"/>
<path class="st1" d="M219.8,115.5c-10.6,0-19.9,4.9-26.3,12.5v-11.4h-10.6v101.3h10.6v-42.7c6.3,7.8,15.7,12.8,26.3,12.8
c19.8,0,36.1-16.9,36.1-36.4C255.9,131.8,239.6,115.5,219.8,115.5L219.8,115.5z M220.1,177.5c-13.8,0-26-12.2-26-26
c0-14.1,12.2-25.6,26-25.6c14.1,0,24.7,11.5,24.7,25.6C244.8,165.3,234.2,177.5,220.1,177.5L220.1,177.5z"/>
<path class="st1" d="M302.3,187.9c19.8,0,36.1-16.9,36.1-36.4c0-19.8-16.3-36.1-36.1-36.1c-19.8,0-36.1,16.3-36.1,36.1
C266.2,171,282.5,187.9,302.3,187.9L302.3,187.9z M302.3,125.9c14.1,0,25,11.5,25,25.6c0,13.8-10.9,26-25,26c-14.1,0-25-12.2-25-26
C277.3,137.5,288.2,125.9,302.3,125.9L302.3,125.9z"/>
<path class="st1" d="M365.6,116.6H355v69.6h10.6v-38.5c0-14.2,11.2-21.8,23.6-21.8v-10.4c-9.6,0-17.9,4.1-23.6,10.6V116.6
L365.6,116.6z"/>
<polygon class="st1" points="433.8,126.2 433.8,116.6 418.1,116.6 418.1,89.2 407.5,89.2 407.5,116.6 397.1,116.6 397.1,126.2
407.5,126.2 407.5,186.2 418.1,186.2 418.1,126.2 433.8,126.2 "/>
<path class="st1" d="M478.6,187.9c10.6,0,19.9-5.1,26.3-12.8v11.4h10.6v-69.9h-10.6V128c-6.3-7.6-15.7-12.5-26.3-12.5
c-19.8,0-36.1,16.3-36.1,36.1C442.5,171,458.8,187.9,478.6,187.9L478.6,187.9z M478.2,177.5c-14.1,0-24.7-12.2-24.7-26
c0-14.1,10.6-25.6,24.7-25.6c13.8,0,26,11.5,26,25.6C504.2,165.3,492,177.5,478.2,177.5L478.2,177.5z"/>
<path class="st1" d="M543.6,102.5c4,0,7.4-3.3,7.4-7.6c0-3.8-3.5-7.3-7.4-7.3c-4.3,0-7.6,3.5-7.6,7.3
C536,99.2,539.3,102.5,543.6,102.5L543.6,102.5z M538.2,186.2h11.1v-69.6h-11.1V186.2L538.2,186.2z"/>
<path class="st1" d="M571.6,186.2h10.6v-37c0-15.7,8.7-23.6,22.8-23.3c11.6,0,17.9,6.8,17.9,20.6v39.7h10.6v-39.7
c0-22.2-8.5-31-28.5-31c-9.5,0-17.2,3.5-22.8,9.5v-8.4h-10.6V186.2L571.6,186.2z"/>
<path class="st1" d="M720.7,151.5c0-19.8-16.3-36.1-36.1-36.1c-19.8,0-36.1,16.3-36.1,36.1c0,19.5,16.3,36.4,36.1,36.4
c14.1,0,26.6-8.1,32.4-20.1h-13.1c-4.4,5.7-11.2,9.7-19.3,9.7c-12.3,0-22.3-9.5-24.5-21h60.6L720.7,151.5L720.7,151.5z
M684.6,125.9c12.2,0,22.2,8.9,24.5,20.4h-49.1C662.4,134.8,672.3,125.9,684.6,125.9L684.6,125.9z"/>
<path class="st1" d="M747.9,116.6h-10.6v69.6h10.6v-38.5c0-14.2,11.2-21.8,23.6-21.8v-10.4c-9.7,0-17.9,4.1-23.6,10.6V116.6
L747.9,116.6z"/>
<path class="st1" d="M787.5,187c4.7,0,8.7-4,8.7-8.9c0-4.7-4-8.7-8.7-8.7c-4.9,0-8.9,4-8.9,8.7C778.6,183,782.6,187,787.5,187
L787.5,187z"/>
<path class="st1" d="M823.5,102.5c4,0,7.4-3.3,7.4-7.6c0-3.8-3.5-7.3-7.4-7.3c-4.3,0-7.6,3.5-7.6,7.3
C816,99.2,819.3,102.5,823.5,102.5L823.5,102.5z M818.2,186.2h11.1v-69.6h-11.1V186.2L818.2,186.2z"/>
<path class="st1" d="M882.1,187.9c19.8,0,36.1-16.9,36.1-36.4c0-19.8-16.3-36.1-36.1-36.1c-19.8,0-36.1,16.3-36.1,36.1
C846,171,862.3,187.9,882.1,187.9L882.1,187.9z M882.1,125.9c14.1,0,25,11.5,25,25.6c0,13.8-10.9,26-25,26c-14.1,0-25-12.2-25-26
C857.1,137.5,868,125.9,882.1,125.9L882.1,125.9z"/>
<polygon class="st0" points="77.7,106.5 56.5,106.5 56.5,127.8 77.7,127.8 77.7,106.5 "/>
<polygon class="st0" points="53.8,106.5 32.6,106.5 32.6,127.8 53.8,127.8 53.8,106.5 "/>
<polygon class="st0" points="53.8,130.2 32.6,130.2 32.6,151.5 53.8,151.5 53.8,130.2 "/>
<polygon class="st0" points="77.7,130.2 56.5,130.2 56.5,151.5 77.7,151.5 77.7,130.2 "/>
<polygon class="st0" points="101.5,130.2 80.3,130.2 80.3,151.5 101.5,151.5 101.5,130.2 "/>
<polygon class="st0" points="101.5,95.1 80.3,95.1 80.3,116.4 101.5,116.4 101.5,95.1 "/>
<path class="st0" d="M57.6,210.7c2.9-12.3,14-21.5,27.2-21.5c8.5,0,16.1,3.8,21.3,9.8c4.5-3.1,9.9-4.9,15.8-4.9
c15.4,0,27.9,12.5,27.9,27.9c0,3.2-0.5,6.2-1.5,9.1c3.4,4.6,5.5,10.4,5.5,16.6c0,15.4-12.5,27.9-27.9,27.9c-6.8,0-13-2.4-17.8-6.4
c-5.1,7.1-13.4,11.8-22.8,11.8c-10.8,0-20.2-6.2-24.9-15.2c-1.9,0.4-3.8,0.6-5.8,0.6c-15.4,0-28-12.5-28-27.9s12.5-27.9,28-27.9
C55.6,210.5,56.6,210.5,57.6,210.7L57.6,210.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

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