Compare commits

...

129 Commits

Author SHA1 Message Date
sunportainer
b56531e67d feat/ee-1977/rollup-duplicated-volumes 2021-12-07 23:29:48 +08:00
Hao Zhang
9f5ac154aa feat(stack): make stack created from app template editable EE-1941 (#6104)
feat(stack): make stack from app template editable
2021-12-07 19:46:58 +08:00
Richard Wei
0627e16b35 fix data-cy for k8s cluster menu (#6226)
LGTM
2021-12-07 14:25:20 +13:00
Marcelo Rydel
2a1b8efaed fix(kubeconfig): show kubeconfig download button for non admin users [EE-2123] (#6204)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-12-06 18:40:59 -03:00
cong meng
98972dec0d feat(webhook) EE-2125 send registry auth haeder when update swarms service via webhook (#6220)
* feat(webhook) EE-2125 add some helpers to registry utils

* feat(webhook) EE-2125 persist registryID when creating a webhook

* feat(webhook) EE-2125 send registry auth header when executing a webhook

* feat(webhook) EE-2125 send registryID to backend when creating a service with webhook

* feat(webhook) EE-2125 use the initial registry ID to create webhook on editing service screen

* feat(webhook) EE-2125 update webhook when update registry

* feat(webhook) EE-2125 add endpoint of update webhook

* feat(webhook) EE-2125 code cleanup

* feat(webhook) EE-2125 fix a typo

* feat(webhook) EE-2125 fix circle import issue with unit test

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-12-07 09:11:44 +13:00
Richard Wei
aa8fc52106 fix app templates symbol (#6221) 2021-12-06 19:15:18 +13:00
zees-dev
5839f96787 - standard user cannot delete another users api-keys (#6208) (#6217)
- added new method to get api key by ID
- added tests
2021-12-06 10:21:33 +13:00
zees-dev
7cc28b10a0 fallback to depracted copy text if clipboard api not available (#6200) (#6218) 2021-12-06 10:01:54 +13:00
Prabhat Khera
4aea5690a8 feat(config): add base url support EE-506 (#5999) 2021-12-03 14:34:45 +13:00
sunportainer
335f951e6b Fix(stack)/update StackUpdateGit swagger info to POST EE-2019 (#6176)
* fix/EE-2019/Fix-stackgitupdate-swagger

Co-authored-by: sunportainer <ericsun@SG1.local>
2021-12-02 09:54:38 +08:00
Hao Zhang
42e782452c fix(container): prevent user from editing the portainer container it self EE-917 (#6093)
* fix(container): prevent from editing portainer container

* fix(container): prevent from editing portainer container

* Missing kill operation

* fix(container): enhance creating stack from template

* fix(docker): prevent user from editing the portainer container itself EE-917

* fix(docker): enhance code style

* fix(container): fix issues from code review

* fix(container): enhance creating stack from template

* fix(container): some code review issues

* fix(container): disable leave network when the container is portainer

* fix(container): disable leave network when the container is portainer
2021-12-02 08:41:05 +08:00
Chaim Lev-Ari
d2fe76368a fix(environments): show kubeconfig env list in dark mode (#6156) 2021-12-01 13:58:55 +13:00
Prabhat Khera
aa7d7845c1 verify repositry URL from template json when coping (#6036) (#6111) 2021-12-01 13:54:47 +13:00
cong meng
a86c7046df feat(registry) EE-806 add support for AWS ECR (#6165)
* feat(ecr) EE-806 add support for aws ecr

* feat(ecr) EE-806 fix wrong doc for Ecr Region

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-12-01 13:18:57 +13:00
Matt Hook
ff6185cc81 fix(openamt): fix IsFeatureFlagEnabled, rename MPS Url to MPS Server (#6185)
Co-authored-by: cheloRydel <marcelorydel26@gmail.com>
2021-12-01 12:35:47 +13:00
Matt Hook
f360392d39 Revert "fix(openamt): fix IsFeatureFlagEnabled, rename MPS Url to MPS Server [INT-6] (#6172)" (#6182)
This reverts commit c267355759.
2021-12-01 11:20:20 +13:00
Marcelo Rydel
fa44a62c4a fix(react): use ctrl directive in WidgetTitle component [EE-2118] (#6181) 2021-11-30 18:22:39 -03:00
huib-portainer
2a384d4c64 Update endpointItem.html (#6142)
feat(home): show cpu and ram for non local endpoints EE-2077
2021-11-30 18:46:38 +13:00
LP B
b6fbf8eecc fix(k8s/ingress): ensure new ports are only added to ingress only if app is published via ingress (#6153)
* fix(k8s/ingress): ensure new ports are only added to ingress only if app is published via ingress

* refactor(k8s/ingress): removed deleted ports of ingress in a single pass
2021-11-30 17:14:52 +13:00
zees-dev
69c17986d9 feat(api-key/backend): introducing support for api-key based auth EE-978 (#6079)
* feat(access-token): Multi-auth middleware support EE-1891 (#5936)

* AnyAuth middleware initial implementation with tests

* using mux.MiddlewareFunc instead of custom definition

* removed redundant comments

* - ExtractBearerToken bouncer func made private
- changed helm token handling functionality to use jwt service to convert token to jwt string
- updated tests
- fixed helm list broken test due to missing token in request context

* rename mwCheckAuthentication -> mwCheckJWTAuthentication

* - introduce initial api-key auth support using X-API-KEY header
- added tests to validate x-api-key request header presence

* updated core mwAuthenticatedUser middleware to support multiple auth paradigms

* - simplified anyAuth middleware
- enforcing authmiddleware to implement verificationFunc interface
- created tests for middleware

* simplify bouncer

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>

* feat(api-key): user-access-token generation endpoint EE-1889 EE-1888 EE-1895 (#6012)

* user-access-token generation endpoint

* fix comment

* - introduction of apikey service
- seperation of repository from service logic - called in handler

* fixed tests

* - fixed api key prefix
- added tests

* added another test for digest matching

* updated swagger spec for access token creation

* api key response returns raw key and struct - easing testability

* test for api key prefix length

* added another TODO to middleware

* - api-key prefix rune -> string (rune does not auto-encode when response sent back to client)
- digest -> pointer as we want to allow nil values and omit digest in responses (when nil)

* - updated apikey struct
- updated apikey service to support all common operations
- updated apikey repo
- integration of apikey service into bouncer
- added test for all apikey service functions
- boilerplate code for apikey service integration

* - user access token generation tests
- apiKeyLookup updated to support query params
- added api-key tests for query params
- added api-key tests for apiKeyLookup

* get and remove access token handlers

* get and remove access token handler tests

* - delete user deletes all associated api keys
- tests for this functionality

* removed redundant []byte cast

* automatic api-key eviction set within cache for 1 hour

* fixed bug with loop var using final value

* fixed service comment

* ignore bolt error responses

* case-insensitive query param check

* simplified query var assignment

* - added GetAPIKey func to get by unique id
- updated DeleteAPIKey func to not require user ID
- updated tests

* GenerateRandomKey helper func from github.com/gorilla/securecookie moved to codebase

* json response casing for api-keys fixed

* updating api-key will update the cache

* updated golang LRU cache

* using hashicorps golang-LRU cache for api keys

* simplified jwt check in create user access token

* fixed api-key update logic on cache miss

* Prefix generated api-keys with `ptr_` (#6067)

* prefix api-keys with 'ptr_'

* updated apikey description

* refactor

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>

* helm list test refactor

* fixed user delete test

* reduce test nil pointer errors

* using correct http 201 created status code for token creation; updated tests

* fixed swagger doc user id path param for user access token based endpoints

* added api-key security openapi spec to existing jwt secured endpoints (#6091)

* fixed flaky test

* apikey datecreated and lastused attrs converted to unix timestamp

* feat(user): added access token datatable. (#6124)

* feat(user): added access token datatable.

* feat(tokens): only display lastUsed time when it is not the default date

* Update app/portainer/views/account/accountController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/views/account/accountController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/views/account/accountController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/components/datatables/access-tokens-datatable/accessTokensDatatableController.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* Update app/portainer/services/api/userService.js

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* feat(improvements): proposed datatable improvements to speed up dev time (#6138)

* modal code update

* updated datatable filenames, updated controller to be default class export

* fix(access-token): code improvement.

Co-authored-by: zees-dev <63374656+zees-dev@users.noreply.github.com>

* feat(apikeys): create access token view initial implementation EE-1886 (#6129)

* CopyButton implementation

* Code component implementation

* ToolTip component migration to another folder

* TextTip component implementation - continued

* form Heading component

* Button component updated to be more dynamic

* copybutton - small size

* form control pass tip error

* texttip small text

* CreateAccessToken react feature initial implementation

* create user access token angularjs view implementation

* registration of CreateAccessToken component in AngularJS

* user token generation API request moved to angular service, method passed down instead

* consistent naming of access token operations; clustered similar code together

* any user can add access token

* create access token page routing

* moved code component to the correct location

* removed isadmin check as all functionality applicable to all users

* create access token angular view moved up a level

* fixed PR issues, updated PR

* addressed PR issues/improvements

* explicit hr for horizontal line

* fixed merge conflict storybook build breaking

* - apikey test
- cache test

* addressed testing issues:
- description validations
- remove token description link on table

* fix(api-keys): user role change evicts user keys in cache EE-2113 (#6168)

* user role change evicts user api keys in cache

* EvictUserKeyCache -> InvalidateUserKeyCache

* godoc for InvalidateUserKeyCache func

* additional test line

* disable add access token button after adding token to prevent spam

Co-authored-by: Dmitry Salakhov <to@dimasalakhov.com>
Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com>
2021-11-30 15:31:16 +13:00
Sven Dowideit
120584909c fix(docker-event-display): EE-1968: support (event_name)[:extra info] for all event Actions, and append it to the output details (#6092)
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-30 09:59:55 +10:00
Richard Wei
c24dc3112b fix(registry): fix order of registries in drop down menu EE-1939 (#5960)
Co-authored-by: Prabhat Khera <prabhat.khera@portainer.io>
2021-11-30 11:03:08 +13:00
Prabhat Khera
1e80061186 feat(docker): allow docker container resource settings without restart EE-1942 (#6065)
Co-authored-by: sam <sam@allofword>
Co-authored-by: sam@gemibook <huapox@126.com>
Co-authored-by: Prabhat Khera <prabhat.khera@gmail.com>
2021-11-30 11:01:09 +13:00
Marcelo Rydel
c267355759 fix(openamt): fix IsFeatureFlagEnabled, rename MPS Url to MPS Server [INT-6] (#6172) 2021-11-29 18:44:33 -03:00
Marcelo Rydel
47c1af93ea feat(openamt): Configuration of the OpenAMT capability [INT-6] (#6071)
Co-authored-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-29 10:06:50 -03:00
Agneev Mukherjee
ab0849d0f3 fix (ui): set correct dimensions for Apple touch image asset (#5888) 2021-11-29 09:21:07 -03:00
fhanportainer
3f31d4b00b feat(ce): fix small issues for Highlight Business Edition feature (#6043)
* feat(logs): added orange border to export button.

* fix(ldap): fixed connectivity check radio button alignment issue

* fix(be-feature): added box shadow to limited feature border

* fix(be-feature): fixed hide internal auth toggle issue.

* feat(ldap): added isLimitedFeatureSelfContained config to ldap-custom-admin-group and test-login components

* feat(auth): moved save settings button in auth page to a component.

* feat(oauth): use saveSettingsButton component in oauth

* feat(oauth): use saveSettingsButton component in internal auth

* feat(oauth): use saveSettingsButton component in MS active directory auth

* feat(oauth): use saveSettingsButton component in ldap auth

* feat(auth): added new component to index.js

* removed css inline styles.

* feat(ad): added disable check method to ad auth.

* feat(ldap): moved save settings disable method to parent component

* removed inline styles.

* fix(ldap): fixed test login misalignment issue

* fix(ldap): pass isLdapFormValid function from page component

* fix(ldap): made the toggle button in custom admin group orange when it's limited to CE.

* fix(auth): fixed save setting button in Microsoft Active Directory auth

* fix(ldap): made the assign admin toggle bright orange
2021-11-29 10:41:21 +13:00
Sven Dowideit
18c323185e Revert "EE-1877: Windows command line for agent isn't the same as on Linux (#5895)" (#6159)
This reverts commit 6255e8d4b5.
2021-11-26 13:32:05 +10:00
fhanportainer
7768d27cfc fix(k8s): fixed force redeployment info text (#6113)
* fix(k8s): fixed force redeployment info text

* feat(stack): added infor text when automatic update is off.
2021-11-26 09:53:53 +13:00
Hao Zhang
97b8da9d10 fix(logs): copy issues caused by extra CR (#6150) 2021-11-25 12:46:58 +08:00
Marcelo Rydel
0928d1832d chore(build): allow darwin binaries download [EE-2070] (#6120) 2021-11-24 11:05:59 -03:00
Matt Hook
d091b343b9 feat(migrations): add more logging EE-2071 (#6141)
* add stacktrace when recovering a panic

* add logging to the migrations

* use string format

* add context around why we return stacktrace
2021-11-24 15:58:43 +13:00
Sven Dowideit
2555dfc78b chore(build): add a PORTAINER_FLAGS env var for yarn so I can default a password, and enable feature flags (#6116)
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-24 12:45:42 +10:00
Hao Zhang
761d2a11d3 fix(helm): fix go to top issue (#6134) 2021-11-23 18:27:34 +08:00
Sven Dowideit
6255e8d4b5 EE-1877: Windows command line for agent isn't the same as on Linux (#5895)
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-23 17:51:14 +10:00
Chaim Lev-Ari
830286c332 feat(app): introduce input-group component [EE-2062] (#6135) 2021-11-23 07:16:50 +02:00
Connor Lanigan
9ad626b36e fix(webhooks): support image names containing a port number (#4526) (#5970)
This fixes a bug where image/registry names that contain a port number were inadvertently truncated (because port numbers are specified with a colon, just like the image tag).

For example, updating an image named `registry.example.com:5000/myimage:oldtag` with the new image tag `newtag` was incorrectly transformed into `registry.example.com:newtag`
2021-11-23 07:15:59 +02:00
Richard Wei
a598b2d72d change the namespace selector behavior (#5768) 2021-11-23 09:51:02 +13:00
Marcelo Rydel
6be1ff4d9c feature(kubeconfig): access to all kube environment contexts from within the Portainer UI [EE-1727] (#5966) 2021-11-22 15:05:09 -03:00
Chaim Lev-Ari
c0a4727114 feat(app): introduce input list component [EE-2003] (#6123) 2021-11-22 18:13:40 +02:00
sunportainer
cea634a7aa fix(stack): support removing duplicated stacks EE-1962 (#6068)
* fix/EE-1962/cannot-same-stack-name handle multiple names duplicate case

Co-authored-by: Eric Sun <ericsun@SG1.local>
2021-11-22 12:23:56 +08:00
itsconquest
5f2e3452e4 fix(settings): move custom logo url [EE-1698] (#5984) 2021-11-22 09:47:10 +13:00
itsconquest
aa15b34add fix(logs): strip trailing comma [EE-1957] (#5975) 2021-11-22 09:45:18 +13:00
Marcelo Rydel
06d25d1491 feat(app): Slider component [EE-2004] (#6024) 2021-11-21 12:49:18 +02:00
Chaim Lev-Ari
8e83a95996 feat(app): introduce button selector component [EE-2004] (#6112) 2021-11-21 11:39:26 +02:00
Hao Zhang
17a20cb2c6 fix(sidebar): clear current endpoint if deleted [EE-873] (#6052) 2021-11-19 12:21:46 +08:00
Hao Zhang
b596d0febd fix(logs): extra CRs in downloading container logs EE-1973 (#6041) 2021-11-19 12:21:16 +08:00
J.F.Gratton
33871eb447 style(images): remove files filter from upload image task EE-1944 2021-11-19 11:27:21 +13:00
zees-dev
183304853e feat(openapi): github workflow to generate and validate openapi spec EE-2056 (#6101)
* github workflow to generate and validate openapi spec

* updated github workflow name to remove spaces and be more explicit

* added swagger-cli globally to reduce dep installation times

* removed redundant webhook payload in GET request

* fixed edgeGroupList OAS3 response model

* updated CI pipeline to convert OAS2 to OAS3 and validate OAS3 instead

* updated pipeline name to be more explicit

* removed redundant swagger-cli dependency as we are using swagger2openapi only in github CI

* fixed bug with no validation - using swagger-cli to validate
2021-11-19 09:44:08 +13:00
Prabhat Khera
0042c7c1d9 fix(home): poll endpoints if one is down EE-1755 (#6006) 2021-11-18 11:01:01 +13:00
Prabhat Khera
80af93afec feat(images): allow tags when importing docker image EE-1737 (#5883) 2021-11-18 10:58:38 +13:00
Prabhat Khera
988069df56 update help link in sidebar and readme (#6082) 2021-11-18 10:57:17 +13:00
Marcelo Rydel
0ee403c1b2 feat(app): added Input components [EE-2007] (#6028) 2021-11-17 20:32:57 +02:00
Matt Hook
b280eb6997 fix(dockerhub-migration): prevent duplicate migrated dockerhub entries EE-2042 (#6083)
* fix(migration) make dockerhub registry migration idempotent EE-2042

* add missing changes to make updateDockerhubToDB32 idempotent

* add tests for bad migrations
2021-11-17 13:21:09 +13:00
deviantony
761e102b2f update README and issue template 2021-11-16 18:52:36 +00:00
Chaim Lev-Ari
5bd157f8fc refactor(app): wrap react with StrictMode [EE-2023] (#6075) 2021-11-16 18:33:51 +02:00
Chaim Lev-Ari
bcaf20caca refactor(app/widgets): create widgets react components [EE-1813] (#6097) 2021-11-16 16:51:49 +02:00
Chaim Lev-Ari
1a6af5d58f feat(app): add tooltip component [EE-2047] (#6088) 2021-11-16 16:11:18 +02:00
Marcelo Rydel
41993ad378 feat(app): create react button component [EE-1948] (#6022)
Co-authored-by: Chaim Lev-Ari <chiptus@gmail.com>
2021-11-16 14:33:01 +02:00
Hui
6b91a813f0 fix(k8s): k8s deployment manifest file placeholder EE-1936 2021-11-17 00:44:09 +13:00
fhanportainer
d64cab0c50 fix(k8s): fixed force redeployment info text (#6042) 2021-11-16 10:45:37 +13:00
Marcelo Rydel
048613a0c5 feature(kubeconfig): Do not invalidate kubeconfig upon Portainer restarting [EE-1854] (#5905) 2021-11-15 18:45:20 -03:00
Sven Dowideit
22b72fb6e3 fix(docker-event-display): support the exec exited event type (#5990)
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-11-15 10:00:37 +10:00
zees-dev
7d92aa1971 Unit tests for enableFeaturesFromFlags function (#6063)
* - exporting BoolPairs CLI func
- added tests for enableFeaturesFromFlags function

* Add a test that uses a feature flag to add change the outcome of code - and test persistence, as that's the current implementation

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

* Minor comment updates

Co-authored-by: Sven Dowideit <sven.dowideit@portainer.io>
Co-authored-by: Stéphane Busso <stephane.busso@gmail.com>
2021-11-15 09:00:25 +10:00
Richard Wei
9e9a4ca4cc feat(ui): add option to sync portainer with system theme EE-1788 (#5812)
* add option to sync portainer with system theme
2021-11-15 11:50:21 +13:00
andres-portainer
a2886115b8 fix(custom-templates): avoid creation of template if the compose file does not exist EE-1470 (#6011)
fix(custom-templates): avoid creation of template if the compose file does not exist EE-1470
2021-11-12 11:02:10 -03:00
Richard Wei
cc3b1face2 fix docker pull limit not showed to non admin (#6066) 2021-11-12 15:57:12 +13:00
cong meng
1157849b70 fix(edge) EE-2027 cannot connect to edge agent with high network latency (#6064)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-11-12 13:12:09 +13:00
Hui
98b8d6d0b2 fix(stack): git credential got reset when updating docker stack EE-1967 2021-11-12 11:52:09 +13:00
andres-portainer
e126f63965 feat(openamt): add feature flag for OpenAMT [INT-5] (#6049)
feat(openamt): add feature flag for OpenAMT [INT-5]
2021-11-11 15:49:50 -03:00
Richard Wei
af0d637414 fix cluster setup page route (#6020) 2021-11-08 14:32:36 +13:00
fhanportainer
ebfabe6c47 fix(k8s): check if app has stack before removing. (#5919) 2021-11-04 08:30:19 +13:00
Chaim Lev-Ari
85a6a80722 feat(app): introduce react configurations [EE-1809] (#5953) 2021-11-03 12:41:59 +02:00
Luis Louis
b285219a58 fix(frontend): Validate previous if the containerPort is not undefined [EE-1555] (#5827) 2021-11-03 11:25:40 +13:00
Matt Hook
3fb8a232b8 feat(update): highlight business edition feature auto update change window EE-1482 (#5961)
* remove unuse component from ce (#5930)

* update wording to Change Window

Co-authored-by: Richard Wei <54336863+WaysonWei@users.noreply.github.com>
Co-authored-by: waysonwei <degui.wei@gmail.com>
2021-11-03 09:57:21 +13:00
andres-portainer
28f71e486a fix(filesystem): harden the filesystem service to avoid path traversal attacks EE-1922 (#5957)
fix(filesystem): harden the filesystem service to avoid path traversal attacks EE-1922
2021-11-01 08:01:03 -03:00
Matt Hook
c763219f74 update version to 2.9.3 (#6007) 2021-11-01 13:27:06 +13:00
Matt Hook
8f4589e535 fix(migration): bubble up recovered panic in new error EE-1971 (#5997)
* fix(migration): bubble up recovered panic in new error EE-1971

* improve code and add comments
2021-10-30 22:32:57 +13:00
Hui
0caf5ca59e fix(migration): ignore volumes with no created timestamp EE-1966 2021-10-30 11:09:11 +13:00
Matt Hook
cec8f34ae9 fix(helm): allow clearing global helm repo EE-1965 (#5991)
* fix(helm): allow clearing global helm repo EE-1965

* fix(helm): show hint if global helm repo is blank EE-1965

* fix(helm): skip loading charts if repo is blank EE-1965

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-29 11:46:55 +13:00
Hui
71de07bbea feat(stack): support force update for git-based stacks EE-1611 2021-10-29 10:35:21 +13:00
Sven Dowideit
76ced401f0 chore(build): reduce the time to run yarn build:server from 1.5minutes, to 10 seconds (#5987)
* reduce the time to run yarn build:server from 1.5minutes, to 10 seconds

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

* add yarn test:server

Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-10-28 21:18:13 +10:00
wheresolivia
33001a8654 add data-cy attribute to helm menu in ce kube sidebar (#5985) 2021-10-27 17:12:12 +13:00
Marcelo Rydel
f738af0f34 fix(stacks): fix missing type prop in stack view [EE-1950] (#5972) 2021-10-26 19:26:13 -03:00
cong meng
5c85c563e1 fix(image) EE-1955 unable to tag image (#5974)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-26 15:22:28 +13:00
Simon Meng
db00390cd2 Merge remote-tracking branch 'origin/release/2.9' into develop
# Conflicts:
#	api/http/handler/websocket/shell_pod.go
#	app/portainer/components/box-selector/box-selector-item/box-selector-item.html
#	app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.html
#	app/portainer/settings/authentication/ldap/ad-settings/ad-settings.html
#	app/portainer/settings/authentication/ldap/index.js
#	app/portainer/settings/authentication/ldap/ldap-settings-custom/ldap-settings-custom.html
#	app/portainer/settings/authentication/ldap/ldap-settings.model.js
#	app/portainer/settings/authentication/ldap/ldap-settings/ldap-settings.controller.js
#	app/portainer/views/settings/authentication/settingsAuthenticationController.js
2021-10-26 10:58:19 +13:00
Marcelo Rydel
32756f9e1b fix(git-stacks): UI bugs when using a PAT when deploying from Git [EE-1731] (#5882) 2021-10-25 18:19:05 -03:00
Sven Dowideit
5ba80c3a44 sorry, wrong place to push to
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-10-22 13:34:19 +10:00
Sven Dowideit
77f73378ea try this, but reset later
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-10-22 13:29:33 +10:00
Marcelo Rydel
734f077861 fix(environments): Endpoint deletion modal missing [EE-1887] (#5904) 2021-10-21 09:23:08 -03:00
Richard Wei
b5ec8c52fb fix standard user not able to access nodes stats (#5951) 2021-10-21 11:56:21 +13:00
Richard Wei
988efe6b02 pull request to develop from EE-1867 (#5958) 2021-10-21 11:55:56 +13:00
Marcelo Rydel
cf60235696 fix(compose): force recreate containers [EE-1906] (#5926) 2021-10-20 09:01:38 -03:00
Stéphane Busso
65cc5342a7 Bump dbversion 2021-10-20 20:48:33 +13:00
Hui
b29961e01e fix(stack): auto update breaks after restarting Portainer EE-1915 2021-10-20 16:01:04 +13:00
Matt Hook
d3cc1a24cc docs(versions): add new tool-versions json file (#5741)
* Add new tool-versions json file to help devs choose the right versions.  Allows querying from doc sites and CI build tools

* add newline at end of file
2021-10-20 12:56:51 +13:00
Snyk bot
fb7cdacbaa fix: build/windows/Dockerfile to reduce vulnerabilities (#5913)
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE313-APKTOOLS-1533754
- https://snyk.io/vuln/SNYK-ALPINE313-OPENSSL-1089239
- https://snyk.io/vuln/SNYK-ALPINE313-OPENSSL-1569446
- https://snyk.io/vuln/SNYK-ALPINE313-OPENSSL-1569448
- https://snyk.io/vuln/SNYK-ALPINE313-OPENSSL-1569448
2021-10-20 08:22:21 +10:00
Matt Hook
ec24826228 pass the correct build arch down not the arch of the machine doing the building EE-1920 (#5929) 2021-10-20 10:02:30 +13:00
cong meng
d18c8d0e88 fix(registry) EE-1861 improve registry selection (#5925)
* 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>

* 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:53 +13:00
Sven Dowideit
623079442f fix(swagger): double quotes in swagger param breaks parser (#5806)
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-10-19 10:25:53 +10:00
fhanportainer
ff87e687ec fix(registry): ignore pull limit in non-docker hub registry. (#5918) 2021-10-19 13:21:54 +13:00
Marcelo Rydel
d4fd295c86 fix(roles): Missing manage access button in user roles [EE-1875] (#5891)
fix(roles): Missing manage access button in user roles [EE-1875]  (#5891)
2021-10-18 18:35:39 -03:00
Richard Wei
62f418836f upgrade chart.js to 2.7.3 & add ticks.precision:0 (#5789) 2021-10-18 22:48:52 +13:00
Richard Wei
ce5ea28727 add warning message for adding registry to namespace (#5793) 2021-10-18 22:46:22 +13:00
Richard Wei
00c7464c25 fix roder for environments in high contrast mode (#5800) 2021-10-18 22:45:00 +13:00
Sven Dowideit
5eced421d5 prevent exception when showing stats on windows container (#5890)
Signed-off-by: Sven Dowideit <sven.dowideit@portainer.io>
2021-10-18 16:36:22 +13:00
Matt Hook
3cde10bcac fix(helm) allow settings to be saved offline EE-1907 (#5907)
* allow settings to be saved offline.  Due to helm repo validation not working for bitnami when offline!

* @hookenz
dont validate the helm repo if the repo hasn't changed or is the default
2021-10-18 15:08:27 +13:00
Chaim Lev-Ari
ba1f0f4018 chore(build): clean gruntfile (#5411) 2021-10-15 09:17:05 +03:00
cong meng
41999e149f fix(edge) EE-1720 activate tunnel and remove proxy cache when needed (#5775)
Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-15 18:13:20 +13:00
andres-portainer
588ce549ad fix(namespaces): remove the stacks from the data store when deleting their corresponding Kubernetes namespace EE-1872 (#5893)
* 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:14:57 -03:00
Marcelo Rydel
edb25ee10d fix(services): pre fill service registry and image [EE-1769] (#5798)
fix(services): pre fill service registry and image [EE-1769]  (#5798)
2021-10-14 09:42:10 -03:00
Marcelo Rydel
12e7aa6b60 fix(environments): don't override with local IP [EE-1561] (#5785)
fix(environments): don't override with local IP [EE-1561] (#5785)
2021-10-14 09:40:14 -03:00
Richard Wei
158cdf596a fix(css): fix decl.moveTo is not a function error in css EE-1744 (#5717)
* fix decl.moveTo is not a function error in css

* Update vendor-override.css
2021-10-13 14:10:37 +13:00
fhanportainer
3d6c6e2604 feat(ldap): LDAP admin auto population EE-568 (#5875)
* feat(ldap): added ldap custom admin group component

* feat(ldap): added ldap custom admin group to LDAP and MS AD pages

* fix(ui): LDAP group search config label

* fix(ldap): removed testing code.

* fix(ldap): fixed default text in ldap custom admin group component
2021-10-13 11:29:00 +13:00
Marcelo Rydel
1ee363f8c9 overrite stack name for update (#5743) 2021-10-12 18:48:28 -03:00
Marcelo Rydel
109b27594a save settings draft (#5872) 2021-10-12 14:51:43 -03:00
zees-dev
54d47ebc76 feat(docker/kubernetes): backend docker and kubernetes dependency updates (#5861)
* client-go library update + go mod tidy

* update all k8s methods to include context

* docker/cli updated to v20.10.9 (latest)

* - removed docker/docker to docker/engine replace directive
- go mod tidy

* docker/docker updated to v20.10.9 (latest)
2021-10-12 15:32:14 +13:00
Hui
e6d690e31e fix(swagger) swagger annotations fixes and improvements EE-1205 2021-10-12 12:12:08 +13:00
cong meng
6a67e8142d fix(frontend) prevent notification showing Object Object EE-1745 (#5778)
* fix(frontend) prevent notification showing Object Object EE-1745

* fix(frontend) fix notification args in wrong order EE-1745

* fix(rbac) add metrics rbac for regular users EE-1745

Co-authored-by: Simon Meng <simon.meng@portainer.io>
2021-10-12 10:37:07 +13:00
Chaim Lev-Ari
d93d88fead fix(app): add data-cy to box-selector (#5869) 2021-10-12 10:14:01 +13:00
Richard Wei
685552a661 fix(wizard): fix wizard not visible in dark theme EE-1800 (#5822)
* fix wizard not visible in dark theme
2021-10-08 14:59:01 +13:00
Richard Wei
1b0e58a4e8 fix upload file not selectable on mac (#5808) 2021-10-08 12:17:22 +13:00
Chaim Lev-Ari
151dfe7e65 fix(compose): use tcp for agent proxy EE-1807 (#5854) 2021-10-08 11:59:50 +13:00
Chaim Lev-Ari
ed89587cb9 fix(ldap): enable user/group setting in custom ldap (#5855) 2021-10-08 10:43:04 +13:00
zees-dev
dad762de9f added swagger docs to websocketShellPodExec (#5840) 2021-10-07 15:32:07 +13:00
Richard Wei
661931d8b0 fix(template): add name validation for template name EE-1806 (#5823)
* add name validation for tempalte name
2021-10-07 13:02:56 +13:00
Richard Wei
84e57cebc9 fix set namespace to default-namespace (#5820) 2021-10-07 11:06:53 +13:00
Marcelo Rydel
fd9427cd0b remove default value for compose path (#5821) 2021-10-06 10:12:36 -03:00
Chaim Lev-Ari
e60dbba93b feat(app): highlight be provided value [EE-882] (#5703) 2021-10-06 09:24:26 +03:00
585 changed files with 22735 additions and 4689 deletions

View File

@@ -1,13 +0,0 @@
{
"plugins": ["lodash", "angularjs-annotate"],
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"useBuiltIns": "entry",
"corejs": "2"
}
]
]
}

View File

@@ -17,12 +17,76 @@ plugins:
parserOptions:
ecmaVersion: 2018
sourceType: module
project: './tsconfig.json'
ecmaFeatures:
modules: true
rules:
no-control-regex: off
no-control-regex: 'off'
no-empty: warn
no-empty-function: warn
no-useless-escape: off
import/order: error
no-useless-escape: 'off'
import/order:
[
'error',
{
pathGroups: [{ pattern: '@/**', group: 'internal' }, { pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroupsExcludedImportTypes: ['internal'],
},
]
settings:
'import/resolver':
alias:
map:
- ['@', './app']
extensions: ['.js', '.ts', '.tsx']
overrides:
- files:
- app/**/*.ts{,x}
parserOptions:
project: './tsconfig.json'
parser: '@typescript-eslint/parser'
plugins:
- '@typescript-eslint'
extends:
- airbnb
- airbnb-typescript
- 'plugin:eslint-comments/recommended'
- 'plugin:react-hooks/recommended'
- 'plugin:react/jsx-runtime'
- 'plugin:@typescript-eslint/recommended'
- 'plugin:@typescript-eslint/eslint-recommended'
- 'plugin:promise/recommended'
- prettier # should be last
settings:
react:
version: 'detect'
rules:
import/order:
['error', { pathGroups: [{ pattern: '@/**', group: 'internal' }], groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 'newlines-between': 'always' }]
func-style: [error, 'declaration']
import/prefer-default-export: off
no-use-before-define: ['error', { functions: false }]
'@typescript-eslint/no-use-before-define': ['error', { functions: false }]
no-shadow: 'off'
'@typescript-eslint/no-shadow': off
jsx-a11y/no-autofocus: warn
react/forbid-prop-types: off
react/require-default-props: off
react/no-array-index-key: off
react/jsx-filename-extension: [0]
import/no-extraneous-dependencies: ['error', { devDependencies: true }]
'@typescript-eslint/explicit-module-boundary-types': off
'@typescript-eslint/no-unused-vars': 'error'
'@typescript-eslint/no-explicit-any': 'error'
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either' }]
- files:
- app/**/*.test.*
extends:
- 'plugin:jest/recommended'
- 'plugin:jest/style'
env:
'jest/globals': true

View File

@@ -1,5 +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?
- name: Portainer Business Edition - Get 5 nodes free
url: https://portainer.io/pricing/take5
about: Portainer Business Edition has more features, more support and you can now get 5 nodes free for as long as you want.

View File

@@ -0,0 +1,53 @@
name: Validate
on:
pull_request:
branches:
- master
- develop
- 'release/*'
jobs:
openapi-spec:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Setup Node v14
uses: actions/setup-node@v2
with:
node-version: 14
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Setup Go v1.17.3
uses: actions/setup-go@v2
with:
go-version: '^1.17.3'
- name: Prebuild docs
run: yarn prebuild:docs
- name: Build OpenAPI 2.0 Spec
run: yarn build:docs
# Install dependencies globally to bypass installing all frontend deps
- name: Install swagger2openapi and swagger-cli
run: yarn global add swagger2openapi @apidevtools/swagger-cli
# OpenAPI2.0 does not support multiple body params (which we utilise in some of our handlers).
# OAS3.0 however does support multiple body params - hence its best to convert the generated OAS 2.0
# to OAS 3.0 and validate the output of generated OAS 3.0 instead.
- name: Convert OpenAPI 2.0 to OpenAPI 3.0 and validate spec
run: yarn validate:docs

2
.gitignore vendored
View File

@@ -3,9 +3,11 @@ bower_components
dist
portainer-checksum.txt
api/cmd/portainer/portainer*
storybook-static
.tmp
**/.vscode/settings.json
**/.vscode/tasks.json
*.DS_Store
.eslintcache
__debug_bin

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
dist

View File

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

31
.storybook/main.js Normal file
View File

@@ -0,0 +1,31 @@
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
module.exports = {
stories: ['../app/**/*.stories.mdx', '../app/**/*.stories.@(ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
{
name: '@storybook/addon-postcss',
options: {
cssLoaderOptions: {
importLoaders: 1,
modules: {
localIdentName: '[path][name]__[local]',
auto: true,
exportLocalsConvention: 'camelCaseOnly',
},
},
},
},
],
webpackFinal: (config) => {
config.resolve.plugins = [
...(config.resolve.plugins || []),
new TsconfigPathsPlugin({
extensions: config.resolve.extensions,
}),
];
return config;
},
};

11
.storybook/preview.js Normal file
View File

@@ -0,0 +1,11 @@
import '../app/assets/css';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};

View File

@@ -150,6 +150,7 @@
"// @description ",
"// @description **Access policy**: ",
"// @tags ",
"// @security ApiKeyAuth",
"// @security jwt",
"// @accept json",
"// @produce json",

View File

@@ -75,7 +75,7 @@ The feature request process is similar to the bug report process but has an extr
![portainer_featurerequest_workflow](https://user-images.githubusercontent.com/5485061/45727229-5ad39f00-bbf5-11e8-9550-16ba66c50615.png)
## Build Portainer locally
## Build and run Portainer locally
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
@@ -85,7 +85,7 @@ Install dependencies with yarn:
$ yarn
```
Then build and run the project:
Then build and run the project in a Docker container:
```sh
$ yarn start
@@ -95,6 +95,14 @@ Portainer can now be accessed at <https://localhost:9443>.
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
### Build customisation
You can customise the following settings:
- `PORTAINER_DATA`: The host dir or volume name used by portainer (default is `/tmp/portainer`, which won't persist over reboots).
- `PORTAINER_PROJECT`: The root dir of the repository - `${portainerRoot}/dist/` is imported into the container to get the build artifacts and external tools (defaults to `your current dir`).
- `PORTAINER_FLAGS`: a list of flags to be used on the portainer commandline, in the form `--admin-password=<pwd hash> --feat fdo=false --feat open-amt` (default: `""`).
## Adding api docs
When adding a new resource (or a route handler), we should add a new tag to api/http/handler/handler.go#L136 like this:
@@ -112,6 +120,7 @@ When adding a new route to an existing handler use the following as a template (
// @description
// @description **Access policy**:
// @tags
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@@ -2,13 +2,15 @@
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/app/assets/images/portainer-github-banner.png?raw=true' />
</p>
**Portainer CE** is a lightweight universal management GUI that can be used to **easily** manage Docker, Swarm, Kubernetes and ACI environments. It is designed to be as **simple** to deploy as it is to use.
**Portainer Community Edition** is a lightweight service delivery platform for containerized applications that can be used to manage Docker, Swarm, Kubernetes and ACI environments. It is designed to be as simple to deploy as it is to use. The application allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a smart GUI and/or an extensive API.
Portainer consists of a single container that can run on any cluster. It can be deployed as a Linux container or a Windows native container.
**Portainer** allows you to manage all your orchestrator resources (containers, images, volumes, networks and more) through a super-simple graphical interface.
**Portainer Business Edition** builds on the open-source base and includes a range of advanced features and functions (like RBAC and Support) that are specific to the needs of business users.
A fully supported version of Portainer is available for business use. Visit http://www.portainer.io to learn more
- [Compare Portainer CE and Compare Portainer BE](https://portainer.io/products)
- [Take5 get 5 free nodes of Portainer Business for as long as you want them](https://portainer.io/pricing/take5)
- [Portainer BE install guide](https://install.portainer.io)
## Demo
@@ -20,12 +22,11 @@ Please note that the public demo cluster is **reset every 15min**.
Portainer CE is updated regularly. We aim to do an update release every couple of months.
**The latest version of Portainer is 2.6.x** And you can find the release notes [here.](https://www.portainer.io/blog/new-portainer-ce-2.6.0-release)
Portainer is on version 2, the second number denotes the month of release.
**The latest version of Portainer is 2.9.x**. Portainer is on version 2, the second number denotes the month of release.
## Getting started
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
- [Deploy Portainer](https://docs.portainer.io/v/ce-2.9/start/install)
- [Documentation](https://documentation.portainer.io)
- [Contribute to the project](https://documentation.portainer.io/contributing/instructions/)
@@ -41,7 +42,7 @@ View [this](https://www.portainer.io/products) table to see all of the Portainer
Portainer CE is an open source project and is supported by the community. You can buy a supported version of Portainer at portainer.io
Learn more about Portainers community support channels [here.](https://www.portainer.io/help_about)
Learn more about Portainers community support channels [here.](https://www.portainer.io/community_help)
- Issues: https://github.com/portainer/portainer/issues
- Slack (chat): [https://portainer.slack.com/](https://join.slack.com/t/portainer/shared_invite/zt-txh3ljab-52QHTyjCqbe5RibC2lcjKA)
@@ -51,15 +52,15 @@ You can join the Portainer Community by visiting community.portainer.io. This wi
## Reporting bugs and contributing
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request. We need all the help we can get!
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request.
## Security
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
## WORK FOR US
## 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/or visit our [careers page](https://portainer.io/careers).
## Privacy

30
api/apikey/apikey.go Normal file
View File

@@ -0,0 +1,30 @@
package apikey
import (
"crypto/rand"
"io"
portainer "github.com/portainer/portainer/api"
)
// APIKeyService represents a service for managing API keys.
type APIKeyService interface {
HashRaw(rawKey string) []byte
GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error)
GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error)
GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error)
GetDigestUserAndKey(digest []byte) (portainer.User, portainer.APIKey, error)
UpdateAPIKey(apiKey *portainer.APIKey) error
DeleteAPIKey(apiKeyID portainer.APIKeyID) error
InvalidateUserKeyCache(userId portainer.UserID) bool
}
// generateRandomKey generates a random key of specified length
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
func generateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}

50
api/apikey/apikey_test.go Normal file
View File

@@ -0,0 +1,50 @@
package apikey
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_generateRandomKey(t *testing.T) {
is := assert.New(t)
tests := []struct {
name string
wantLenth int
}{
{
name: "Generate a random key of length 16",
wantLenth: 16,
},
{
name: "Generate a random key of length 32",
wantLenth: 32,
},
{
name: "Generate a random key of length 64",
wantLenth: 64,
},
{
name: "Generate a random key of length 128",
wantLenth: 128,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := generateRandomKey(tt.wantLenth)
is.Equal(tt.wantLenth, len(got))
})
}
t.Run("Generated keys are unique", func(t *testing.T) {
keys := make(map[string]bool)
for i := 0; i < 100; i++ {
key := generateRandomKey(8)
_, ok := keys[string(key)]
is.False(ok)
keys[string(key)] = true
}
})
}

69
api/apikey/cache.go Normal file
View File

@@ -0,0 +1,69 @@
package apikey
import (
lru "github.com/hashicorp/golang-lru"
portainer "github.com/portainer/portainer/api"
)
const defaultAPIKeyCacheSize = 1024
// entry is a tuple containing the user and API key associated to an API key digest
type entry struct {
user portainer.User
apiKey portainer.APIKey
}
// apiKeyCache is a concurrency-safe, in-memory cache which primarily exists for to reduce database roundtrips.
// We store the api-key digest (keys) and the associated user and key-data (values) in the cache.
// This is required because HTTP requests will contain only the api-key digest in the x-api-key request header;
// digest value must be mapped to a portainer user (and respective key data) for validation.
// This cache is used to avoid multiple database queries to retrieve these user/key associated to the digest.
type apiKeyCache struct {
// cache type [string]entry cache (key: string(digest), value: user/key entry)
// note: []byte keys are not supported by golang-lru Cache
cache *lru.Cache
}
// NewAPIKeyCache creates a new cache for API keys
func NewAPIKeyCache(cacheSize int) *apiKeyCache {
cache, _ := lru.New(cacheSize)
return &apiKeyCache{cache: cache}
}
// Get returns the user/key associated to an api-key's digest
// This is required because HTTP requests will contain the digest of the API key in header,
// the digest value must be mapped to a portainer user.
func (c *apiKeyCache) Get(digest []byte) (portainer.User, portainer.APIKey, bool) {
val, ok := c.cache.Get(string(digest))
if !ok {
return portainer.User{}, portainer.APIKey{}, false
}
tuple := val.(entry)
return tuple.user, tuple.apiKey, true
}
// Set persists a user/key entry to the cache
func (c *apiKeyCache) Set(digest []byte, user portainer.User, apiKey portainer.APIKey) {
c.cache.Add(string(digest), entry{
user: user,
apiKey: apiKey,
})
}
// Delete evicts a digest's user/key entry key from the cache
func (c *apiKeyCache) Delete(digest []byte) {
c.cache.Remove(string(digest))
}
// InvalidateUserKeyCache loops through all the api-keys associated to a user and removes them from the cache
func (c *apiKeyCache) InvalidateUserKeyCache(userId portainer.UserID) bool {
present := false
for _, k := range c.cache.Keys() {
user, _, _ := c.Get([]byte(k.(string)))
if user.ID == userId {
present = c.cache.Remove(k)
}
}
return present
}

181
api/apikey/cache_test.go Normal file
View File

@@ -0,0 +1,181 @@
package apikey
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)
func Test_apiKeyCacheGet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.cache.Add(string("foo"), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string(""), entry{user: portainer.User{}, apiKey: portainer.APIKey{}})
tests := []struct {
digest []byte
found bool
}{
{
digest: []byte("foo"),
found: true,
},
{
digest: []byte(""),
found: true,
},
{
digest: []byte("bar"),
found: false,
},
}
for _, test := range tests {
t.Run(string(test.digest), func(t *testing.T) {
_, _, found := keyCache.Get(test.digest)
is.Equal(test.found, found)
})
}
}
func Test_apiKeyCacheSet(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
// pre-populate cache
keyCache.Set([]byte("bar"), portainer.User{ID: 2}, portainer.APIKey{})
keyCache.Set([]byte("foo"), portainer.User{ID: 1}, portainer.APIKey{})
// overwrite existing entry
keyCache.Set([]byte("foo"), portainer.User{ID: 3}, portainer.APIKey{})
val, ok := keyCache.cache.Get(string("bar"))
is.True(ok)
tuple := val.(entry)
is.Equal(portainer.User{ID: 2}, tuple.user)
val, ok = keyCache.cache.Get(string("foo"))
is.True(ok)
tuple = val.(entry)
is.Equal(portainer.User{ID: 3}, tuple.user)
}
func Test_apiKeyCacheDelete(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
t.Run("Delete an existing entry", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.Delete([]byte("foo"))
_, ok := keyCache.cache.Get(string("foo"))
is.False(ok)
})
t.Run("Delete a non-existing entry", func(t *testing.T) {
nonPanicFunc := func() { keyCache.Delete([]byte("non-existent-key")) }
is.NotPanics(nonPanicFunc)
})
}
func Test_apiKeyCacheLRU(t *testing.T) {
is := assert.New(t)
tests := []struct {
name string
cacheLen int
key []string
foundKeys []string
evictedKeys []string
}{
{
name: "Cache length is 1, add 2 keys",
cacheLen: 1,
key: []string{"foo", "bar"},
foundKeys: []string{"bar"},
evictedKeys: []string{"foo"},
},
{
name: "Cache length is 1, add 3 keys",
cacheLen: 1,
key: []string{"foo", "bar", "baz"},
foundKeys: []string{"baz"},
evictedKeys: []string{"foo", "bar"},
},
{
name: "Cache length is 2, add 3 keys",
cacheLen: 2,
key: []string{"foo", "bar", "baz"},
foundKeys: []string{"bar", "baz"},
evictedKeys: []string{"foo"},
},
{
name: "Cache length is 2, add 4 keys",
cacheLen: 2,
key: []string{"foo", "bar", "baz", "qux"},
foundKeys: []string{"baz", "qux"},
evictedKeys: []string{"foo", "bar"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
keyCache := NewAPIKeyCache(test.cacheLen)
for _, key := range test.key {
keyCache.Set([]byte(key), portainer.User{ID: 1}, portainer.APIKey{})
}
for _, key := range test.foundKeys {
_, _, found := keyCache.Get([]byte(key))
is.True(found, "Key %s not found", key)
}
for _, key := range test.evictedKeys {
_, _, found := keyCache.Get([]byte(key))
is.False(found, "key %s should have been evicted", key)
}
})
}
}
func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
keyCache := NewAPIKeyCache(10)
t.Run("Removes users keys from cache", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
_, ok = keyCache.cache.Get(string("foo"))
is.False(ok)
})
t.Run("Does not affect other keys", func(t *testing.T) {
keyCache.cache.Add(string("foo"), entry{user: portainer.User{ID: 1}, apiKey: portainer.APIKey{}})
keyCache.cache.Add(string("bar"), entry{user: portainer.User{ID: 2}, apiKey: portainer.APIKey{}})
ok := keyCache.InvalidateUserKeyCache(1)
is.True(ok)
ok = keyCache.InvalidateUserKeyCache(1)
is.False(ok)
_, ok = keyCache.cache.Get(string("foo"))
is.False(ok)
_, ok = keyCache.cache.Get(string("bar"))
is.True(ok)
})
}

126
api/apikey/service.go Normal file
View File

@@ -0,0 +1,126 @@
package apikey
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
)
const portainerAPIKeyPrefix = "ptr_"
var ErrInvalidAPIKey = errors.New("Invalid API key")
type apiKeyService struct {
apiKeyRepository portainer.APIKeyRepository
userRepository portainer.UserService
cache *apiKeyCache
}
func NewAPIKeyService(apiKeyRepository portainer.APIKeyRepository, userRepository portainer.UserService) *apiKeyService {
return &apiKeyService{
apiKeyRepository: apiKeyRepository,
userRepository: userRepository,
cache: NewAPIKeyCache(defaultAPIKeyCacheSize),
}
}
// HashRaw computes a hash digest of provided raw API key.
func (a *apiKeyService) HashRaw(rawKey string) []byte {
hashDigest := sha256.Sum256([]byte(rawKey))
return hashDigest[:]
}
// GenerateApiKey generates a raw API key for a user (for one-time display).
// The generated API key is stored in the cache and database.
func (a *apiKeyService) GenerateApiKey(user portainer.User, description string) (string, *portainer.APIKey, error) {
randKey := generateRandomKey(32)
encodedRawAPIKey := base64.StdEncoding.EncodeToString(randKey)
prefixedAPIKey := portainerAPIKeyPrefix + encodedRawAPIKey
hashDigest := a.HashRaw(prefixedAPIKey)
apiKey := &portainer.APIKey{
UserID: user.ID,
Description: description,
Prefix: prefixedAPIKey[:7],
DateCreated: time.Now().Unix(),
Digest: hashDigest,
}
err := a.apiKeyRepository.CreateAPIKey(apiKey)
if err != nil {
return "", nil, errors.Wrap(err, "Unable to create API key")
}
// persist api-key to cache
a.cache.Set(apiKey.Digest, user, *apiKey)
return prefixedAPIKey, apiKey, nil
}
// GetAPIKey returns an API key by its ID.
func (a *apiKeyService) GetAPIKey(apiKeyID portainer.APIKeyID) (*portainer.APIKey, error) {
return a.apiKeyRepository.GetAPIKey(apiKeyID)
}
// GetAPIKeys returns all the API keys associated to a user.
func (a *apiKeyService) GetAPIKeys(userID portainer.UserID) ([]portainer.APIKey, error) {
return a.apiKeyRepository.GetAPIKeysByUserID(userID)
}
// GetDigestUserAndKey returns the user and api-key associated to a specified hash digest.
// A cache lookup is performed first; if the user/api-key is not found in the cache, respective database lookups are performed.
func (a *apiKeyService) GetDigestUserAndKey(digest []byte) (portainer.User, portainer.APIKey, error) {
// get api key from cache if possible
cachedUser, cachedKey, ok := a.cache.Get(digest)
if ok {
return cachedUser, cachedKey, nil
}
apiKey, err := a.apiKeyRepository.GetAPIKeyByDigest(digest)
if err != nil {
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve API key")
}
user, err := a.userRepository.User(apiKey.UserID)
if err != nil {
return portainer.User{}, portainer.APIKey{}, errors.Wrap(err, "Unable to retrieve digest user")
}
// persist api-key to cache - for quicker future lookups
a.cache.Set(apiKey.Digest, *user, *apiKey)
return *user, *apiKey, nil
}
// UpdateAPIKey updates an API key and in cache and database.
func (a *apiKeyService) UpdateAPIKey(apiKey *portainer.APIKey) error {
user, _, err := a.GetDigestUserAndKey(apiKey.Digest)
if err != nil {
return errors.Wrap(err, "Unable to retrieve API key")
}
a.cache.Set(apiKey.Digest, user, *apiKey)
return a.apiKeyRepository.UpdateAPIKey(apiKey)
}
// DeleteAPIKey deletes an API key and removes the digest/api-key entry from the cache.
func (a *apiKeyService) DeleteAPIKey(apiKeyID portainer.APIKeyID) error {
// get api-key digest to remove from cache
apiKey, err := a.apiKeyRepository.GetAPIKey(apiKeyID)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Unable to retrieve API key: %d", apiKeyID))
}
// delete the user/api-key from cache
a.cache.Delete(apiKey.Digest)
return a.apiKeyRepository.DeleteAPIKey(apiKeyID)
}
func (a *apiKeyService) InvalidateUserKeyCache(userId portainer.UserID) bool {
return a.cache.InvalidateUserKeyCache(userId)
}

309
api/apikey/service_test.go Normal file
View File

@@ -0,0 +1,309 @@
package apikey
import (
"crypto/sha256"
"log"
"strings"
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/stretchr/testify/assert"
)
func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
is := assert.New(t)
is.Implements((*APIKeyService)(nil), NewAPIKeyService(nil, nil))
}
func Test_GenerateApiKey(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully generates API key", func(t *testing.T) {
desc := "test-1"
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, desc)
is.NoError(err)
is.NotEmpty(rawKey)
is.NotEmpty(apiKey)
is.Equal(desc, apiKey.Description)
})
t.Run("Api key prefix is 7 chars", func(t *testing.T) {
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-2")
is.NoError(err)
is.Equal(rawKey[:7], apiKey.Prefix)
is.Len(apiKey.Prefix, 7)
})
t.Run("Api key has 'ptr_' as prefix", func(t *testing.T) {
rawKey, _, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x")
is.NoError(err)
is.Equal(portainerAPIKeyPrefix, "ptr_")
is.True(strings.HasPrefix(rawKey, "ptr_"))
})
t.Run("Successfully caches API key", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-3")
is.NoError(err)
userFromCache, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(user, userFromCache)
is.Equal(apiKey, &apiKeyFromCache)
})
t.Run("Decoded raw api-key digest matches generated digest", func(t *testing.T) {
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-4")
is.NoError(err)
generatedDigest := sha256.Sum256([]byte(rawKey))
is.Equal(apiKey.Digest, generatedDigest[:])
})
}
func Test_GetAPIKey(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully returns all API keys", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
apiKeyGot, err := service.GetAPIKey(apiKey.ID)
is.NoError(err)
is.Equal(apiKey, apiKeyGot)
})
}
func Test_GetAPIKeys(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully returns all API keys", func(t *testing.T) {
user := portainer.User{ID: 1}
_, _, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, _, err = service.GenerateApiKey(user, "test-2")
is.NoError(err)
keys, err := service.GetAPIKeys(user.ID)
is.NoError(err)
is.Len(keys, 2)
})
}
func Test_GetDigestUserAndKey(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully returns user and api key associated to digest", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
is.Equal(user, userGot)
is.Equal(*apiKey, apiKeyGot)
})
t.Run("Successfully caches user and api key associated to digest", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
is.Equal(user, userGot)
is.Equal(*apiKey, apiKeyGot)
userFromCache, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(userGot, userFromCache)
is.Equal(apiKeyGot, apiKeyFromCache)
})
}
func Test_UpdateAPIKey(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully updates the api-key LastUsed time", func(t *testing.T) {
user := portainer.User{ID: 1}
store.User().CreateUser(&user)
_, apiKey, err := service.GenerateApiKey(user, "test-x")
is.NoError(err)
apiKey.LastUsed = time.Now().UTC().Unix()
err = service.UpdateAPIKey(apiKey)
is.NoError(err)
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
log.Println(apiKey)
log.Println(apiKeyGot)
is.Equal(apiKey.LastUsed, apiKeyGot.LastUsed)
})
t.Run("Successfully updates api-key in cache upon api-key update", func(t *testing.T) {
_, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x2")
is.NoError(err)
_, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, apiKeyFromCache)
apiKey.LastUsed = time.Now().UTC().Unix()
is.NotEqual(*apiKey, apiKeyFromCache)
err = service.UpdateAPIKey(apiKey)
is.NoError(err)
_, updatedAPIKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, updatedAPIKeyFromCache)
})
}
func Test_DeleteAPIKey(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully updates the api-key", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
is.NoError(err)
is.Equal(*apiKey, apiKeyGot)
err = service.DeleteAPIKey(apiKey.ID)
is.NoError(err)
_, _, err = service.GetDigestUserAndKey(apiKey.Digest)
is.Error(err)
})
t.Run("Successfully removes api-key from cache upon deletion", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, apiKeyFromCache)
err = service.DeleteAPIKey(apiKey.ID)
is.NoError(err)
_, _, ok = service.cache.Get(apiKey.Digest)
is.False(ok)
})
}
func Test_InvalidateUserKeyCache(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
service := NewAPIKeyService(store.APIKeyRepository(), store.User())
t.Run("Successfully updates evicts keys from cache", func(t *testing.T) {
// generate api keys
user := portainer.User{ID: 1}
_, apiKey1, err := service.GenerateApiKey(user, "test-1")
is.NoError(err)
_, apiKey2, err := service.GenerateApiKey(user, "test-2")
is.NoError(err)
// verify api keys are present in cache
_, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest)
is.True(ok)
is.Equal(*apiKey1, apiKeyFromCache)
_, apiKeyFromCache, ok = service.cache.Get(apiKey2.Digest)
is.True(ok)
is.Equal(*apiKey2, apiKeyFromCache)
// evict cache
ok = service.InvalidateUserKeyCache(user.ID)
is.True(ok)
// verify users keys have been flushed from cache
_, _, ok = service.cache.Get(apiKey1.Digest)
is.False(ok)
_, _, ok = service.cache.Get(apiKey2.Digest)
is.False(ok)
})
t.Run("User key eviction does not affect other users keys", func(t *testing.T) {
// generate keys for 2 users
user1 := portainer.User{ID: 1}
_, apiKey1, err := service.GenerateApiKey(user1, "test-1")
is.NoError(err)
user2 := portainer.User{ID: 2}
_, apiKey2, err := service.GenerateApiKey(user2, "test-2")
is.NoError(err)
// verify keys in cache
_, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest)
is.True(ok)
is.Equal(*apiKey1, apiKeyFromCache)
_, apiKeyFromCache, ok = service.cache.Get(apiKey2.Digest)
is.True(ok)
is.Equal(*apiKey2, apiKeyFromCache)
// evict key of single user from cache
ok = service.cache.InvalidateUserKeyCache(user1.ID)
is.True(ok)
// verify user1 key has been flushed from cache
_, _, ok = service.cache.Get(apiKey1.Digest)
is.False(ok)
// verify user2 key is still in cache
_, _, ok = service.cache.Get(apiKey2.Digest)
is.True(ok)
})
}

View File

@@ -0,0 +1,61 @@
package ecr
import (
"context"
"encoding/base64"
"fmt"
"strings"
"time"
)
func (s *Service) GetEncodedAuthorizationToken() (token *string, expiry *time.Time, err error) {
getAuthorizationTokenOutput, err := s.client.GetAuthorizationToken(context.TODO(), nil)
if err != nil {
return
}
if len(getAuthorizationTokenOutput.AuthorizationData) == 0 {
err = fmt.Errorf("AuthorizationData is empty")
return
}
authData := getAuthorizationTokenOutput.AuthorizationData[0]
token = authData.AuthorizationToken
expiry = authData.ExpiresAt
return
}
func (s *Service) GetAuthorizationToken() (token *string, expiry *time.Time, err error) {
tokenEncodedStr, expiry, err := s.GetEncodedAuthorizationToken()
if err != nil {
return
}
tokenByte, err := base64.StdEncoding.DecodeString(*tokenEncodedStr)
if err != nil {
return
}
tokenStr := string(tokenByte)
token = &tokenStr
return
}
func (s *Service) ParseAuthorizationToken(token string) (username string, password string, err error) {
if len(token) == 0 {
return
}
splitToken := strings.Split(token, ":")
if len(splitToken) < 2 {
err = fmt.Errorf("invalid ECR authorization token")
return
}
username = splitToken[0]
password = splitToken[1]
return
}

32
api/aws/ecr/ecr.go Normal file
View File

@@ -0,0 +1,32 @@
package ecr
import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/ecr"
)
type (
Service struct {
accessKey string
secretKey string
region string
client *ecr.Client
}
)
func NewService(accessKey, secretKey, region string) *Service {
options := ecr.Options{
Region: region,
Credentials: aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(accessKey, secretKey, "")),
}
client := ecr.New(options)
return &Service{
accessKey: accessKey,
secretKey: secretKey,
region: region,
client: client,
}
}

View File

@@ -0,0 +1,137 @@
package apikeyrepository
import (
"bytes"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/bolt/internal"
"github.com/boltdb/bolt"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "api_key"
)
// Service represents a service for managing api-key data.
type Service struct {
connection *internal.DbConnection
}
// NewService creates a new instance of a service.
func NewService(connection *internal.DbConnection) (*Service, error) {
err := internal.CreateBucket(connection, BucketName)
if err != nil {
return nil, err
}
return &Service{
connection: connection,
}, nil
}
// GetAPIKeysByUserID returns a slice containing all the APIKeys a user has access to.
func (service *Service) GetAPIKeysByUserID(userID portainer.UserID) ([]portainer.APIKey, error) {
var result = make([]portainer.APIKey, 0)
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var record portainer.APIKey
err := internal.UnmarshalObject(v, &record)
if err != nil {
return err
}
if record.UserID == userID {
result = append(result, record)
}
}
return nil
})
return result, err
}
// GetAPIKeyByDigest returns the API key for the associated digest.
// Note: there is a 1-to-1 mapping of api-key and digest
func (service *Service) GetAPIKeyByDigest(digest []byte) (*portainer.APIKey, error) {
var result portainer.APIKey
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var record portainer.APIKey
err := internal.UnmarshalObject(v, &record)
if err != nil {
return err
}
if bytes.Equal(record.Digest, digest) {
result = record
return nil
}
}
return nil
})
return &result, err
}
// CreateAPIKey creates a new APIKey object.
func (service *Service) CreateAPIKey(record *portainer.APIKey) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
id, _ := bucket.NextSequence()
record.ID = portainer.APIKeyID(id)
data, err := internal.MarshalObject(record)
if err != nil {
return err
}
return bucket.Put(internal.Itob(int(record.ID)), data)
})
}
// GetAPIKey retrieves an existing APIKey object by api key ID.
func (service *Service) GetAPIKey(keyID portainer.APIKeyID) (*portainer.APIKey, error) {
var apiKey *portainer.APIKey
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
item := bucket.Get(internal.Itob(int(keyID)))
if item == nil {
return errors.ErrObjectNotFound
}
err := internal.UnmarshalObject(item, &apiKey)
if err != nil {
return err
}
return nil
})
return apiKey, err
}
func (service *Service) UpdateAPIKey(key *portainer.APIKey) error {
identifier := internal.Itob(int(key.ID))
return internal.UpdateObject(service.connection, BucketName, identifier, key)
}
func (service *Service) DeleteAPIKey(ID portainer.APIKeyID) error {
return service.connection.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
return bucket.Delete(internal.Itob(int(ID)))
})
}

View File

@@ -5,6 +5,7 @@ import (
"path"
"time"
"github.com/portainer/portainer/api/bolt/apikeyrepository"
"github.com/portainer/portainer/api/bolt/helmuserrepository"
"github.com/boltdb/bolt"
@@ -60,6 +61,7 @@ type Store struct {
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
APIKeyRepositoryService *apikeyrepository.Service
ScheduleService *schedule.Service
SettingsService *settings.Service
SSLSettingsService *ssl.Service

View File

@@ -2,6 +2,7 @@ package bolt
import (
"fmt"
"runtime/debug"
"github.com/portainer/portainer/api/cli"
@@ -18,13 +19,17 @@ const beforePortainerVersionUpgradeBackup = "portainer.db.bak"
var migrateLog = plog.NewScopedLog("bolt, migrate")
// FailSafeMigrate backup and restore DB if migration fail
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) error {
func (store *Store) FailSafeMigrate(migrator *migrator.Migrator) (err error) {
defer func() {
if err := recover(); err != nil {
migrateLog.Info(fmt.Sprintf("Error during migration, recovering [%v]", err))
if e := recover(); e != nil {
store.Rollback(true)
// return error with cause and stacktrace (recover() doesn't include a stacktrace)
err = fmt.Errorf("%v %s", e, string(debug.Stack()))
}
}()
// !Important: we must use a named return value in the function definition and not a local
// !variable referenced from the closure or else the return value will be incorrectly set
return migrator.Migrate()
}

View File

@@ -237,6 +237,7 @@ func (m *Migrator) Migrate() error {
// Portainer 1.24.0
if m.currentDBVersion < 23 {
migrateLog.Info("Migrating to DB 23")
err := m.updateTagsToDBVersion23()
if err != nil {
return migrationError(err, "updateTagsToDBVersion23")
@@ -250,6 +251,7 @@ func (m *Migrator) Migrate() error {
// Portainer 1.24.1
if m.currentDBVersion < 24 {
migrateLog.Info("Migrating to DB 24")
err := m.updateSettingsToDB24()
if err != nil {
return migrationError(err, "updateSettingsToDB24")
@@ -258,6 +260,7 @@ func (m *Migrator) Migrate() error {
// Portainer 2.0.0
if m.currentDBVersion < 25 {
migrateLog.Info("Migrating to DB 25")
err := m.updateSettingsToDB25()
if err != nil {
return migrationError(err, "updateSettingsToDB25")
@@ -271,6 +274,7 @@ func (m *Migrator) Migrate() error {
// Portainer 2.1.0
if m.currentDBVersion < 26 {
migrateLog.Info("Migrating to DB 26")
err := m.updateEndpointSettingsToDB25()
if err != nil {
return migrationError(err, "updateEndpointSettingsToDB25")
@@ -279,6 +283,7 @@ func (m *Migrator) Migrate() error {
// Portainer 2.2.0
if m.currentDBVersion < 27 {
migrateLog.Info("Migrating to DB 27")
err := m.updateStackResourceControlToDB27()
if err != nil {
return migrationError(err, "updateStackResourceControlToDB27")
@@ -287,6 +292,7 @@ func (m *Migrator) Migrate() error {
// Portainer 2.6.0
if m.currentDBVersion < 30 {
migrateLog.Info("Migrating to DB 30")
err := m.migrateDBVersionToDB30()
if err != nil {
return migrationError(err, "migrateDBVersionToDB30")
@@ -301,8 +307,9 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 2.9.1
// Portainer 2.9.1, 2.9.2
if m.currentDBVersion < 33 {
migrateLog.Info("Migrating to DB 33")
err := m.migrateDBVersionToDB33()
if err != nil {
return migrationError(err, "migrateDBVersionToDB33")
@@ -311,11 +318,20 @@ func (m *Migrator) Migrate() error {
// Portainer 2.10
if m.currentDBVersion < 34 {
migrateLog.Info("Migrating to DB 34")
if err := m.migrateDBVersionToDB34(); err != nil {
return migrationError(err, "migrateDBVersionToDB34")
}
}
// Portainer 2.9.3 (yep out of order, but 2.10 is EE only)
if m.currentDBVersion < 35 {
migrateLog.Info("Migrating to DB 35")
if err := m.migrateDBVersionToDB35(); err != nil {
return migrationError(err, "migrateDBVersionToDB35")
}
}
err = m.versionService.StoreDBVersion(portainer.DBVersion)
if err != nil {
return migrationError(err, "StoreDBVersion")

View File

@@ -1,8 +1,9 @@
package migrator
import "github.com/portainer/portainer/api"
import portainer "github.com/portainer/portainer/api"
func (m *Migrator) updateTagsToDBVersion23() error {
migrateLog.Info("Updating tags")
tags, err := m.tagService.Tags()
if err != nil {
return err
@@ -20,6 +21,7 @@ func (m *Migrator) updateTagsToDBVersion23() error {
}
func (m *Migrator) updateEndpointsAndEndpointGroupsToDBVersion23() error {
migrateLog.Info("Updating endpoints and endpoint groups")
tags, err := m.tagService.Tags()
if err != nil {
return err

View File

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

View File

@@ -1,10 +1,12 @@
package migrator
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
)
func (m *Migrator) updateSettingsToDB25() error {
migrateLog.Info("Updating settings")
legacySettings, err := m.settingsService.Settings()
if err != nil {
return err

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package migrator
func (m *Migrator) migrateDBVersionToDB30() error {
migrateLog.Info("Updating legacy settings")
if err := m.migrateSettingsToDB30(); err != nil {
return err
}
@@ -13,6 +14,7 @@ func (m *Migrator) migrateSettingsToDB30() error {
if err != nil {
return err
}
legacySettings.OAuthSettings.SSO = false
legacySettings.OAuthSettings.LogoutURI = ""
return m.settingsService.UpdateSettings(legacySettings)

View File

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

View File

@@ -16,6 +16,7 @@ func (m *Migrator) migrateSettingsToDB33() error {
return err
}
migrateLog.Info("Setting default kubectl shell image")
settings.KubectlShellImage = portainer.DefaultKubectlShellImage
return m.settingsService.UpdateSettings(settings)
}

View File

@@ -5,6 +5,7 @@ import (
)
func (m *Migrator) migrateDBVersionToDB34() error {
migrateLog.Info("Migrating stacks")
err := migrateStackEntryPoint(m.stackService)
if err != nil {
return err

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ package bolt
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt/apikeyrepository"
"github.com/portainer/portainer/api/bolt/customtemplate"
"github.com/portainer/portainer/api/bolt/dockerhub"
"github.com/portainer/portainer/api/bolt/edgegroup"
@@ -155,6 +156,12 @@ func (store *Store) initServices() error {
}
store.UserService = userService
apiKeyService, err := apikeyrepository.NewService(store.connection)
if err != nil {
return err
}
store.APIKeyRepositoryService = apiKeyService
versionService, err := version.NewService(store.connection)
if err != nil {
return err
@@ -231,6 +238,11 @@ func (store *Store) Role() portainer.RoleService {
return store.RoleService
}
// APIKeyRepository gives access to the api-key data management layer
func (store *Store) APIKeyRepository() portainer.APIKeyRepository {
return store.APIKeyRepositoryService
}
// Settings gives access to the Settings data management layer
func (store *Store) Settings() portainer.SettingsService {
return store.SettingsService

View File

@@ -44,3 +44,17 @@ func (service *Service) Settings() (*portainer.Settings, error) {
func (service *Service) UpdateSettings(settings *portainer.Settings) error {
return internal.UpdateObject(service.connection, BucketName, []byte(settingsKey), settings)
}
func (service *Service) IsFeatureFlagEnabled(feature portainer.Feature) bool {
settings, err := service.Settings()
if err != nil {
return false
}
featureFlagSetting, ok := settings.FeatureFlagSettings[feature]
if ok {
return featureFlagSetting
}
return false
}

View File

@@ -77,6 +77,31 @@ func (service *Service) StackByName(name string) (*portainer.Stack, error) {
return stack, err
}
// Stacks returns an array containing all the stacks with same name
func (service *Service) StacksByName(name string) ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)
err := service.connection.View(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BucketName))
cursor := bucket.Cursor()
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
var t portainer.Stack
err := internal.UnmarshalObject(v, &t)
if err != nil {
return err
}
if t.Name == name {
stacks = append(stacks, t)
}
}
return nil
})
return stacks, err
}
// Stacks returns an array containing all the stacks.
func (service *Service) Stacks() ([]portainer.Stack, error) {
var stacks = make([]portainer.Stack, 0)

View File

@@ -150,3 +150,9 @@ func (service *Service) CreateWebhook(webhook *portainer.Webhook) error {
return bucket.Put(internal.Itob(int(webhook.ID)), data)
})
}
// UpdateWebhook update a webhook.
func (service *Service) UpdateWebhook(ID portainer.WebhookID, webhook *portainer.Webhook) error {
identifier := internal.Itob(int(ID))
return internal.UpdateObject(service.connection, BucketName, identifier, webhook)
}

View File

@@ -3,6 +3,7 @@ package chisel
import (
"context"
"fmt"
"github.com/portainer/portainer/api/http/proxy"
"log"
"net/http"
"strconv"
@@ -32,6 +33,7 @@ type Service struct {
snapshotService portainer.SnapshotService
chiselServer *chserver.Server
shutdownCtx context.Context
ProxyManager *proxy.Manager
}
// NewService returns a pointer to a new instance of Service
@@ -215,18 +217,13 @@ func (service *Service) checkTunnels() {
}
}
if len(tunnel.Jobs) > 0 {
endpointID, err := strconv.Atoi(item.Key)
if err != nil {
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
continue
}
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
} else {
service.tunnelDetailsMap.Remove(item.Key)
endpointID, err := strconv.Atoi(item.Key)
if err != nil {
log.Printf("[ERROR] [chisel,conversion] Invalid environment identifier (id: %s): %s", item.Key, err)
continue
}
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
}
}

View File

@@ -59,6 +59,12 @@ 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.EdgeAgentActive {
// update the LastActivity
service.SetTunnelStatusToActive(endpoint.ID)
}
if tunnel.Status == portainer.EdgeAgentIdle || tunnel.Status == portainer.EdgeAgentManagementRequired {
err := service.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
@@ -74,9 +80,9 @@ func (service *Service) GetActiveTunnel(endpoint *portainer.Endpoint) (*portaine
endpoint.EdgeCheckinInterval = settings.EdgeAgentCheckinInterval
}
waitForAgentToConnect := time.Duration(endpoint.EdgeCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
time.Sleep(2 * time.Duration(endpoint.EdgeCheckinInterval) * time.Second)
}
tunnel = service.GetTunnelDetails(endpoint.ID)
return tunnel, nil
@@ -112,6 +118,8 @@ func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
key := strconv.Itoa(int(endpointID))
service.tunnelDetailsMap.Set(key, tunnel)
service.ProxyManager.DeleteEndpointProxy(endpointID)
}
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified environment(endpoint).

View File

@@ -36,6 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(),
FeatureFlags: BoolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
@@ -54,6 +55,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),
BaseURL: kingpin.Flag("base-url", "Base URL parameter such as portainer if running portainer as http://yourdomain.com/portainer/.").Short('b').Default(defaultBaseURL).String(),
}
kingpin.Parse()

View File

@@ -19,4 +19,5 @@ const (
defaultSSLCertPath = "/certs/portainer.crt"
defaultSSLKeyPath = "/certs/portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
)

View File

@@ -17,4 +17,5 @@ const (
defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSnapshotInterval = "5m"
defaultBaseURL = "/"
)

View File

@@ -1,11 +1,12 @@
package cli
import (
"github.com/portainer/portainer/api"
portainer "github.com/portainer/portainer/api"
"fmt"
"gopkg.in/alecthomas/kingpin.v2"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
)
type pairList []portainer.Pair

45
api/cli/pairlistbool.go Normal file
View File

@@ -0,0 +1,45 @@
package cli
import (
portainer "github.com/portainer/portainer/api"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
)
type pairListBool []portainer.Pair
// Set implementation for a list of portainer.Pair
func (l *pairListBool) Set(value string) error {
p := new(portainer.Pair)
// default to true. example setting=true is equivalent to setting
parts := strings.SplitN(value, "=", 2)
if len(parts) != 2 {
p.Name = parts[0]
p.Value = "true"
} else {
p.Name = parts[0]
p.Value = parts[1]
}
*l = append(*l, *p)
return nil
}
// String implementation for a list of pair
func (l *pairListBool) String() string {
return ""
}
// IsCumulative implementation for a list of pair
func (l *pairListBool) IsCumulative() bool {
return true
}
func BoolPairs(s kingpin.Settings) (target *[]portainer.Pair) {
target = new([]portainer.Pair)
s.SetValue((*pairListBool)(target))
return
}

View File

@@ -2,21 +2,24 @@ package main
import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
"github.com/portainer/libhelm"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/crypto"
"github.com/portainer/portainer/api/docker"
"github.com/portainer/libhelm"
"github.com/portainer/portainer/api/exec"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/git"
"github.com/portainer/portainer/api/hostmanagement/openamt"
"github.com/portainer/portainer/api/http"
"github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy"
@@ -102,8 +105,15 @@ func initComposeStackManager(assetsPath string, configPath string, reverseTunnel
return composeWrapper
}
func initSwarmStackManager(assetsPath string, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService)
func initSwarmStackManager(
assetsPath string,
configPath string,
signatureService portainer.DigitalSignatureService,
fileService portainer.FileService,
reverseTunnelService portainer.ReverseTunnelService,
dataStore portainer.DataStore,
) (portainer.SwarmStackManager, error) {
return exec.NewSwarmStackManager(assetsPath, configPath, signatureService, fileService, reverseTunnelService, dataStore)
}
func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, proxyManager *proxy.Manager, assetsPath string) portainer.KubernetesDeployer {
@@ -114,6 +124,10 @@ func initHelmPackageManager(assetsPath string) (libhelm.HelmPackageManager, erro
return libhelm.NewHelmPackageManager(libhelm.HelmConfig{BinaryPath: assetsPath})
}
func initAPIKeyService(datastore portainer.DataStore) apikey.APIKeyService {
return apikey.NewAPIKeyService(datastore.APIKeyRepository(), datastore.User())
}
func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) {
settings, err := dataStore.Settings().Settings()
if err != nil {
@@ -237,6 +251,49 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
return nil
}
// enableFeaturesFromFlags turns on or off feature flags
// e.g. portainer --feat open-amt --feat fdo=true ... (defaults to true)
// note, settings are persisted to the DB. To turn off `--feat open-amt=false`
func enableFeaturesFromFlags(dataStore portainer.DataStore, flags *portainer.CLIFlags) error {
settings, err := dataStore.Settings().Settings()
if err != nil {
return err
}
if settings.FeatureFlagSettings == nil {
settings.FeatureFlagSettings = make(map[portainer.Feature]bool)
}
// loop through feature flags to check if they are supported
for _, feat := range *flags.FeatureFlags {
var correspondingFeature *portainer.Feature
for i, supportedFeat := range portainer.SupportedFeatureFlags {
if strings.EqualFold(feat.Name, string(supportedFeat)) {
correspondingFeature = &portainer.SupportedFeatureFlags[i]
}
}
if correspondingFeature == nil {
return fmt.Errorf("unknown feature flag '%s'", feat.Name)
}
featureState, err := strconv.ParseBool(feat.Value)
if err != nil {
return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name)
}
if featureState {
log.Printf("Feature %v : on", *correspondingFeature)
} else {
log.Printf("Feature %v : off", *correspondingFeature)
}
settings.FeatureFlagSettings[*correspondingFeature] = featureState
}
return dataStore.Settings().UpdateSettings(settings)
}
func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error {
private, public, err := fileService.LoadKeyPair()
if err != nil {
@@ -412,17 +469,26 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
log.Fatal(err)
}
apiKeyService := initAPIKeyService(dataStore)
jwtService, err := initJWTService(dataStore)
if err != nil {
log.Fatalf("failed initializing JWT service: %v", err)
}
err = enableFeaturesFromFlags(dataStore, flags)
if err != nil {
log.Fatalf("failed enabling feature flag: %v", err)
}
ldapService := initLDAPService()
oauthService := initOAuthService()
gitService := initGitService()
openAMTService := openamt.NewService(dataStore)
cryptoService := initCryptoService()
digitalSignatureService := initDigitalSignatureService()
@@ -467,11 +533,13 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
reverseTunnelService.ProxyManager = proxyManager
dockerConfigPath := fileService.GetDockerConfigPath()
composeStackManager := initComposeStackManager(*flags.Assets, dockerConfigPath, reverseTunnelService, proxyManager)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService)
swarmStackManager, err := initSwarmStackManager(*flags.Assets, dockerConfigPath, digitalSignatureService, fileService, reverseTunnelService, dataStore)
if err != nil {
log.Fatalf("failed initializing swarm stack manager: %s", err)
}
@@ -504,7 +572,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
adminPasswordHash := ""
if *flags.AdminPasswordFile != "" {
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
content, err := fileService.GetFileContent(*flags.AdminPasswordFile, "")
if err != nil {
log.Fatalf("failed getting admin password file: %v", err)
}
@@ -566,11 +634,13 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
KubernetesDeployer: kubernetesDeployer,
HelmPackageManager: helmPackageManager,
CryptoService: cryptoService,
APIKeyService: apiKeyService,
JWTService: jwtService,
FileService: fileService,
LDAPService: ldapService,
OAuthService: oauthService,
GitService: gitService,
OpenAMTService: openAMTService,
ProxyManager: proxyManager,
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
KubeConfigService: kubeConfigService,
@@ -583,6 +653,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
BaseURL: *flags.BaseURL,
}
}

View File

@@ -0,0 +1,114 @@
package main
import (
"fmt"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/cli"
"github.com/stretchr/testify/assert"
"gopkg.in/alecthomas/kingpin.v2"
)
type mockKingpinSetting string
func (m mockKingpinSetting) SetValue(value kingpin.Value) {
value.Set(string(m))
}
func Test_enableFeaturesFromFlags(t *testing.T) {
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
tests := []struct {
featureFlag string
isSupported bool
}{
{"test", false},
{"openamt", false},
{"open-amt", true},
{"oPeN-amT", true},
{"fdo", true},
{"FDO", true},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s succeeds:%v", test.featureFlag, test.isSupported), func(t *testing.T) {
mockKingpinSetting := mockKingpinSetting(test.featureFlag)
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
err := enableFeaturesFromFlags(store, flags)
if test.isSupported {
is.NoError(err)
} else {
is.Error(err)
}
})
}
t.Run("passes for all supported feature flags", func(t *testing.T) {
for _, flag := range portainer.SupportedFeatureFlags {
mockKingpinSetting := mockKingpinSetting(flag)
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
err := enableFeaturesFromFlags(store, flags)
is.NoError(err)
}
})
}
const FeatTest portainer.Feature = "optional-test"
func optionalFunc(dataStore portainer.DataStore) string {
// TODO: this is a code smell - finding out if a feature flag is enabled should not require having access to the store, and asking for a settings obj.
// ideally, the `if` should look more like:
// if featureflags.FlagEnabled(FeatTest) {}
settings, err := dataStore.Settings().Settings()
if err != nil {
return err.Error()
}
if settings.FeatureFlagSettings[FeatTest] {
return "enabled"
}
return "disabled"
}
func Test_optionalFeature(t *testing.T) {
portainer.SupportedFeatureFlags = append(portainer.SupportedFeatureFlags, FeatTest)
is := assert.New(t)
store, teardown := bolt.MustNewTestStore(true)
defer teardown()
// Enable the test feature
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
mockKingpinSetting := mockKingpinSetting(FeatTest)
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
err := enableFeaturesFromFlags(store, flags)
is.NoError(err)
is.Equal("enabled", optionalFunc(store))
})
// Same store, so the feature flag should still be enabled
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
is.Equal("enabled", optionalFunc(store))
})
// disable the test feature
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
mockKingpinSetting := mockKingpinSetting(FeatTest + "=false")
flags := &portainer.CLIFlags{FeatureFlags: cli.BoolPairs(mockKingpinSetting)}
err := enableFeaturesFromFlags(store, flags)
is.NoError(err)
is.Equal("disabled", optionalFunc(store))
})
// Same store, so feature flag should still be disabled
t.Run(fmt.Sprintf("%s succeeds:%v", FeatTest, true), func(t *testing.T) {
is.Equal("disabled", optionalFunc(store))
})
}

View File

@@ -91,7 +91,11 @@ func createEdgeClient(endpoint *portainer.Endpoint, signatureService portainer.D
headers[portainer.PortainerAgentTargetHeader] = nodeName
}
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
tunnel, err := reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return nil, err
}
endpointURL := fmt.Sprintf("http://127.0.0.1:%d", tunnel.Port)
return client.NewClientWithOpts(

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/pkg/errors"
@@ -16,6 +15,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/internal/stackutils"
)
// ComposeStackManager is a wrapper for docker-compose binary
@@ -58,7 +58,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
return errors.Wrap(err, "failed to create env file")
}
filePaths := getStackFiles(stack)
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Deploy(ctx, stack.ProjectPath, url, stack.Name, filePaths, envFilePath)
return errors.Wrap(err, "failed to deploy a stack")
}
@@ -73,7 +73,7 @@ func (manager *ComposeStackManager) Down(ctx context.Context, stack *portainer.S
defer proxy.Close()
}
filePaths := getStackFiles(stack)
filePaths := stackutils.GetStackFilePaths(stack)
err = manager.deployer.Remove(ctx, stack.ProjectPath, url, stack.Name, filePaths)
return errors.Wrap(err, "failed to remove a stack")
}
@@ -115,27 +115,3 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
return "stack.env", nil
}
// getStackFiles returns list of stack's confile file paths.
// items in the list would be sanitized according to following criterias:
// 1. no empty paths
// 2. no "../xxx" paths that are trying to escape stack folder
// 3. no dir paths
// 4. root paths would be made relative
func getStackFiles(stack *portainer.Stack) []string {
paths := make([]string, 0, len(stack.AdditionalFiles)+1)
for _, p := range append([]string{stack.EntryPoint}, stack.AdditionalFiles...) {
if strings.HasPrefix(p, "/") {
p = `.` + p
}
if p == `` || p == `.` || strings.HasPrefix(p, `..`) || strings.HasSuffix(p, string(filepath.Separator)) {
continue
}
paths = append(paths, p)
}
return paths
}

View File

@@ -64,21 +64,3 @@ func Test_createEnvFile(t *testing.T) {
})
}
}
func Test_getStackFiles(t *testing.T) {
stack := &portainer.Stack{
EntryPoint: "./file", // picks entry point
AdditionalFiles: []string{
``, // ignores empty string
`.`, // ignores .
`..`, // ignores ..
`./dir/`, // ignrores paths that end with trailing /
`/with-root-prefix`, // replaces "root" based paths with relative
`./relative`, // keeps relative paths
`../escape`, // prevents dir escape
},
}
filePaths := getStackFiles(stack)
assert.ElementsMatch(t, filePaths, []string{`./file`, `./with-root-prefix`, `./relative`})
}

View File

@@ -12,6 +12,7 @@ import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/internal/stackutils"
)
@@ -22,17 +23,25 @@ type SwarmStackManager struct {
signatureService portainer.DigitalSignatureService
fileService portainer.FileService
reverseTunnelService portainer.ReverseTunnelService
dataStore portainer.DataStore
}
// NewSwarmStackManager initializes a new SwarmStackManager service.
// It also updates the configuration of the Docker CLI binary.
func NewSwarmStackManager(binaryPath, configPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) {
func NewSwarmStackManager(
binaryPath, configPath string,
signatureService portainer.DigitalSignatureService,
fileService portainer.FileService,
reverseTunnelService portainer.ReverseTunnelService,
datastore portainer.DataStore,
) (*SwarmStackManager, error) {
manager := &SwarmStackManager{
binaryPath: binaryPath,
configPath: configPath,
signatureService: signatureService,
fileService: fileService,
reverseTunnelService: reverseTunnelService,
dataStore: datastore,
}
err := manager.updateDockerCLIConfiguration(manager.configPath)
@@ -44,19 +53,36 @@ func NewSwarmStackManager(binaryPath, configPath string, signatureService portai
}
// Login executes the docker login command against a list of registries (including DockerHub).
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoint *portainer.Endpoint) error {
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
if err != nil {
return err
}
for _, registry := range registries {
if registry.Authentication {
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
err = registryutils.EnsureRegTokenValid(manager.dataStore, &registry)
if err != nil {
return err
}
username, password, err := registryutils.GetRegEffectiveCredential(&registry)
if err != nil {
return err
}
registryArgs := append(args, "login", "--username", username, "--password", password, registry.URL)
runCommandAndCaptureStdErr(command, registryArgs, nil, "")
}
}
return nil
}
// Logout executes the docker logout command.
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
if err != nil {
return err
}
args = append(args, "logout")
return runCommandAndCaptureStdErr(command, args, nil, "")
}
@@ -64,7 +90,10 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
// Deploy executes the docker stack deploy command.
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
filePaths := stackutils.GetStackFilePaths(stack)
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
if err != nil {
return err
}
if prune {
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth")
@@ -84,7 +113,10 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
// Remove executes the docker stack rm command.
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
command, args, err := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.configPath, endpoint)
if err != nil {
return err
}
args = append(args, "stack", "rm", stack.Name)
return runCommandAndCaptureStdErr(command, args, nil, "")
}
@@ -108,7 +140,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
return nil
}
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string) {
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, configPath string, endpoint *portainer.Endpoint) (string, []string, error) {
// Assume Linux as a default
command := path.Join(binaryPath, "docker")
@@ -121,7 +153,10 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
endpointURL := endpoint.URL
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
tunnel, err := manager.reverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return "", nil, err
}
endpointURL = fmt.Sprintf("tcp://127.0.0.1:%d", tunnel.Port)
}
@@ -141,7 +176,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
}
}
return command, args
return command, args, nil
}
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
@@ -175,7 +210,7 @@ func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string
func (manager *SwarmStackManager) retrieveConfigurationFromDisk(path string) (map[string]interface{}, error) {
var config map[string]interface{}
raw, err := manager.fileService.GetFileContent(path)
raw, err := manager.fileService.GetFileContent(path, "")
if err != nil {
return make(map[string]interface{}), nil
}

View File

@@ -6,14 +6,14 @@ import (
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
"io"
"os"
"path"
)
const (
@@ -69,12 +69,31 @@ type Service struct {
fileStorePath string
}
// JoinPaths takes a trusted root path and a list of untrusted paths and joins
// them together using directory separators while enforcing that the untrusted
// paths cannot go higher up than the trusted root
func JoinPaths(trustedRoot string, untrustedPaths ...string) string {
if trustedRoot == "" {
trustedRoot = "."
}
p := filepath.Join(trustedRoot, filepath.Join(append([]string{"/"}, untrustedPaths...)...))
// avoid setting a volume name from the untrusted paths
vnp := filepath.VolumeName(p)
if filepath.VolumeName(trustedRoot) == "" && vnp != "" {
return strings.TrimPrefix(strings.TrimPrefix(p, vnp), `\`)
}
return p
}
// NewService initializes a new service. It creates a data directory and a directory to store files
// inside this directory if they don't exist.
func NewService(dataStorePath, fileStorePath string) (*Service, error) {
service := &Service{
dataStorePath: dataStorePath,
fileStorePath: path.Join(dataStorePath, fileStorePath),
fileStorePath: JoinPaths(dataStorePath, fileStorePath),
}
err := os.MkdirAll(dataStorePath, 0755)
@@ -112,12 +131,12 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
// GetBinaryFolder returns the full path to the binary store on the filesystem
func (service *Service) GetBinaryFolder() string {
return path.Join(service.fileStorePath, BinaryStorePath)
return JoinPaths(service.fileStorePath, BinaryStorePath)
}
// GetDockerConfigPath returns the full path to the docker config store on the filesystem
func (service *Service) GetDockerConfigPath() string {
return path.Join(service.fileStorePath, DockerConfigPath)
return JoinPaths(service.fileStorePath, DockerConfigPath)
}
// RemoveDirectory removes a directory on the filesystem.
@@ -128,7 +147,7 @@ func (service *Service) RemoveDirectory(directoryPath string) error {
// GetStackProjectPath returns the absolute path on the FS for a stack based
// on its identifier.
func (service *Service) GetStackProjectPath(stackIdentifier string) string {
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
return JoinPaths(service.wrapFileStore(ComposeStorePath), stackIdentifier)
}
// Copy copies the file on fromFilePath to toFilePath
@@ -194,13 +213,13 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
// StoreStackFileFromBytes creates a subfolder in the ComposeStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
return "", err
}
composeFilePath := path.Join(stackStorePath, fileName)
composeFilePath := JoinPaths(stackStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
@@ -208,25 +227,25 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string
return "", err
}
return path.Join(service.fileStorePath, stackStorePath), nil
return service.wrapFileStore(stackStorePath), nil
}
// GetEdgeStackProjectPath returns the absolute path on the FS for a edge stack based
// on its identifier.
func (service *Service) GetEdgeStackProjectPath(edgeStackIdentifier string) string {
return path.Join(service.fileStorePath, EdgeStackStorePath, edgeStackIdentifier)
return JoinPaths(service.wrapFileStore(EdgeStackStorePath), edgeStackIdentifier)
}
// StoreEdgeStackFileFromBytes creates a subfolder in the EdgeStackStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error) {
stackStorePath := path.Join(EdgeStackStorePath, edgeStackIdentifier)
stackStorePath := JoinPaths(EdgeStackStorePath, edgeStackIdentifier)
err := service.createDirectoryInStore(stackStorePath)
if err != nil {
return "", err
}
composeFilePath := path.Join(stackStorePath, fileName)
composeFilePath := JoinPaths(stackStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
@@ -234,20 +253,20 @@ func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileNam
return "", err
}
return path.Join(service.fileStorePath, stackStorePath), nil
return service.wrapFileStore(stackStorePath), nil
}
// StoreRegistryManagementFileFromBytes creates a subfolder in the
// ExtensionRegistryManagementStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) {
extensionStorePath := path.Join(ExtensionRegistryManagementStorePath, folder)
extensionStorePath := JoinPaths(ExtensionRegistryManagementStorePath, folder)
err := service.createDirectoryInStore(extensionStorePath)
if err != nil {
return "", err
}
file := path.Join(extensionStorePath, fileName)
file := JoinPaths(extensionStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(file, r)
@@ -255,13 +274,13 @@ func (service *Service) StoreRegistryManagementFileFromBytes(folder, fileName st
return "", err
}
return path.Join(service.fileStorePath, file), nil
return service.wrapFileStore(file), nil
}
// StoreTLSFileFromBytes creates a folder in the TLSStorePath and stores a new file from bytes.
// It returns the path to the newly created file.
func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.TLSFileType, data []byte) (string, error) {
storePath := path.Join(TLSStorePath, folder)
storePath := JoinPaths(TLSStorePath, folder)
err := service.createDirectoryInStore(storePath)
if err != nil {
return "", err
@@ -279,13 +298,13 @@ func (service *Service) StoreTLSFileFromBytes(folder string, fileType portainer.
return "", ErrUndefinedTLSFileType
}
tlsFilePath := path.Join(storePath, fileName)
tlsFilePath := JoinPaths(storePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(tlsFilePath, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, tlsFilePath), nil
return service.wrapFileStore(tlsFilePath), nil
}
// GetPathForTLSFile returns the absolute path to a specific TLS file for an environment(endpoint).
@@ -301,17 +320,13 @@ func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSF
default:
return "", ErrUndefinedTLSFileType
}
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
return JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName), nil
}
// DeleteTLSFiles deletes a folder in the TLS store path.
func (service *Service) DeleteTLSFiles(folder string) error {
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
err := os.RemoveAll(storePath)
if err != nil {
return err
}
return nil
storePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder)
return os.RemoveAll(storePath)
}
// DeleteTLSFile deletes a specific TLS file from a folder.
@@ -328,20 +343,19 @@ func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileT
return ErrUndefinedTLSFileType
}
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
filePath := JoinPaths(service.wrapFileStore(TLSStorePath), folder, fileName)
err := os.Remove(filePath)
if err != nil {
return err
}
return nil
return os.Remove(filePath)
}
// GetFileContent returns the content of a file as bytes.
func (service *Service) GetFileContent(filePath string) ([]byte, error) {
content, err := ioutil.ReadFile(filePath)
func (service *Service) GetFileContent(trustedRoot, filePath string) ([]byte, error) {
content, err := os.ReadFile(JoinPaths(trustedRoot, filePath))
if err != nil {
return nil, err
if filePath == "" {
filePath = trustedRoot
}
return nil, fmt.Errorf("could not get the contents of the file '%s'", filePath)
}
return content, nil
@@ -359,7 +373,7 @@ func (service *Service) WriteJSONToFile(path string, content interface{}) error
return err
}
return ioutil.WriteFile(path, jsonContent, 0644)
return os.WriteFile(path, jsonContent, 0644)
}
// FileExists checks for the existence of the specified file.
@@ -369,23 +383,17 @@ func (service *Service) FileExists(filePath string) (bool, error) {
// KeyPairFilesExist checks for the existence of the key files.
func (service *Service) KeyPairFilesExist() (bool, error) {
privateKeyPath := path.Join(service.dataStorePath, PrivateKeyFile)
privateKeyPath := JoinPaths(service.dataStorePath, PrivateKeyFile)
exists, err := service.FileExists(privateKeyPath)
if err != nil {
if err != nil || !exists {
return false, err
}
if !exists {
return false, nil
}
publicKeyPath := path.Join(service.dataStorePath, PublicKeyFile)
publicKeyPath := JoinPaths(service.dataStorePath, PublicKeyFile)
exists, err = service.FileExists(publicKeyPath)
if err != nil {
if err != nil || !exists {
return false, err
}
if !exists {
return false, nil
}
return true, nil
}
@@ -397,12 +405,7 @@ func (service *Service) StoreKeyPair(private, public []byte, privatePEMHeader, p
return err
}
err = service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
if err != nil {
return err
}
return nil
return service.createPEMFileInStore(public, publicPEMHeader, PublicKeyFile)
}
// LoadKeyPair retrieve the content of both key files on disk.
@@ -422,13 +425,13 @@ func (service *Service) LoadKeyPair() ([]byte, []byte, error) {
// createDirectoryInStore creates a new directory in the file store
func (service *Service) createDirectoryInStore(name string) error {
path := path.Join(service.fileStorePath, name)
path := service.wrapFileStore(name)
return os.MkdirAll(path, 0700)
}
// createFile creates a new file in the file store with the content from r.
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
path := path.Join(service.fileStorePath, filePath)
path := service.wrapFileStore(filePath)
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
@@ -437,15 +440,11 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
defer out.Close()
_, err = io.Copy(out, r)
if err != nil {
return err
}
return nil
return err
}
func (service *Service) createPEMFileInStore(content []byte, fileType, filePath string) error {
path := path.Join(service.fileStorePath, filePath)
path := service.wrapFileStore(filePath)
block := &pem.Block{Type: fileType, Bytes: content}
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
@@ -454,18 +453,13 @@ func (service *Service) createPEMFileInStore(content []byte, fileType, filePath
}
defer out.Close()
err = pem.Encode(out, block)
if err != nil {
return err
}
return nil
return pem.Encode(out, block)
}
func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
path := path.Join(service.fileStorePath, filePath)
path := service.wrapFileStore(filePath)
fileContent, err := ioutil.ReadFile(path)
fileContent, err := os.ReadFile(path)
if err != nil {
return nil, err
}
@@ -477,19 +471,19 @@ func (service *Service) getContentFromPEMFile(filePath string) ([]byte, error) {
// GetCustomTemplateProjectPath returns the absolute path on the FS for a custom template based
// on its identifier.
func (service *Service) GetCustomTemplateProjectPath(identifier string) string {
return path.Join(service.fileStorePath, CustomTemplateStorePath, identifier)
return JoinPaths(service.wrapFileStore(CustomTemplateStorePath), identifier)
}
// StoreCustomTemplateFileFromBytes creates a subfolder in the CustomTemplateStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error) {
customTemplateStorePath := path.Join(CustomTemplateStorePath, identifier)
customTemplateStorePath := JoinPaths(CustomTemplateStorePath, identifier)
err := service.createDirectoryInStore(customTemplateStorePath)
if err != nil {
return "", err
}
templateFilePath := path.Join(customTemplateStorePath, fileName)
templateFilePath := JoinPaths(customTemplateStorePath, fileName)
r := bytes.NewReader(data)
err = service.createFileInStore(templateFilePath, r)
@@ -497,32 +491,32 @@ func (service *Service) StoreCustomTemplateFileFromBytes(identifier, fileName st
return "", err
}
return path.Join(service.fileStorePath, customTemplateStorePath), nil
return service.wrapFileStore(customTemplateStorePath), nil
}
// GetEdgeJobFolder returns the absolute path on the filesystem for an Edge job based
// on its identifier.
func (service *Service) GetEdgeJobFolder(identifier string) string {
return path.Join(service.fileStorePath, EdgeJobStorePath, identifier)
return JoinPaths(service.wrapFileStore(EdgeJobStorePath), identifier)
}
// StoreEdgeJobFileFromBytes creates a subfolder in the EdgeJobStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreEdgeJobFileFromBytes(identifier string, data []byte) (string, error) {
edgeJobStorePath := path.Join(EdgeJobStorePath, identifier)
edgeJobStorePath := JoinPaths(EdgeJobStorePath, identifier)
err := service.createDirectoryInStore(edgeJobStorePath)
if err != nil {
return "", err
}
filePath := path.Join(edgeJobStorePath, createEdgeJobFileName(identifier))
filePath := JoinPaths(edgeJobStorePath, createEdgeJobFileName(identifier))
r := bytes.NewReader(data)
err = service.createFileInStore(filePath, r)
if err != nil {
return "", err
}
return path.Join(service.fileStorePath, filePath), nil
return service.wrapFileStore(filePath), nil
}
func createEdgeJobFileName(identifier string) string {
@@ -532,20 +526,14 @@ func createEdgeJobFileName(identifier string) string {
// ClearEdgeJobTaskLogs clears the Edge job task logs
func (service *Service) ClearEdgeJobTaskLogs(edgeJobID string, taskID string) error {
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
err := os.Remove(path)
if err != nil {
return err
}
return nil
return os.Remove(path)
}
// GetEdgeJobTaskLogFileContent fetches the Edge job task logs
func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID string) (string, error) {
path := service.getEdgeJobTaskLogPath(edgeJobID, taskID)
fileContent, err := ioutil.ReadFile(path)
fileContent, err := os.ReadFile(path)
if err != nil {
return "", err
}
@@ -555,20 +543,15 @@ func (service *Service) GetEdgeJobTaskLogFileContent(edgeJobID string, taskID st
// StoreEdgeJobTaskLogFileFromBytes stores the log file
func (service *Service) StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error {
edgeJobStorePath := path.Join(EdgeJobStorePath, edgeJobID)
edgeJobStorePath := JoinPaths(EdgeJobStorePath, edgeJobID)
err := service.createDirectoryInStore(edgeJobStorePath)
if err != nil {
return err
}
filePath := path.Join(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
filePath := JoinPaths(edgeJobStorePath, fmt.Sprintf("logs_%s", taskID))
r := bytes.NewReader(data)
err = service.createFileInStore(filePath, r)
if err != nil {
return err
}
return nil
return service.createFileInStore(filePath, r)
}
func (service *Service) getEdgeJobTaskLogPath(edgeJobID string, taskID string) string {
@@ -582,7 +565,7 @@ func (service *Service) GetTemporaryPath() (string, error) {
return "", err
}
return path.Join(service.fileStorePath, TempPath, uid.String()), nil
return JoinPaths(service.wrapFileStore(TempPath), uid.String()), nil
}
// GetDataStorePath returns path to data folder
@@ -591,12 +574,12 @@ func (service *Service) GetDatastorePath() string {
}
func (service *Service) wrapFileStore(filepath string) string {
return path.Join(service.fileStorePath, filepath)
return JoinPaths(service.fileStorePath, filepath)
}
func defaultCertPathUnderFileStore() (string, string) {
certPath := path.Join(SSLCertPath, DefaultSSLCertFilename)
keyPath := path.Join(SSLCertPath, DefaultSSLKeyFilename)
certPath := JoinPaths(SSLCertPath, DefaultSSLCertFilename)
keyPath := JoinPaths(SSLCertPath, DefaultSSLKeyFilename)
return certPath, keyPath
}

View File

@@ -0,0 +1,70 @@
package filesystem
import "testing"
func TestJoinPaths(t *testing.T) {
var ts = []struct {
trusted string
untrusted string
expected string
}{
{"", "", "."},
{"", ".", "."},
{"", "d/e/f", "d/e/f"},
{"", "./d/e/f", "d/e/f"},
{"", "../d/e/f", "d/e/f"},
{"", "/d/e/f", "d/e/f"},
{"", "../../../etc/shadow", "etc/shadow"},
{".", "", "."},
{".", ".", "."},
{".", "d/e/f", "d/e/f"},
{".", "./d/e/f", "d/e/f"},
{".", "../d/e/f", "d/e/f"},
{".", "/d/e/f", "d/e/f"},
{".", "../../../etc/shadow", "etc/shadow"},
{"./", "", "."},
{"./", ".", "."},
{"./", "d/e/f", "d/e/f"},
{"./", "./d/e/f", "d/e/f"},
{"./", "../d/e/f", "d/e/f"},
{"./", "/d/e/f", "d/e/f"},
{"./", "../../../etc/shadow", "etc/shadow"},
{"a/b/c", "", "a/b/c"},
{"a/b/c", ".", "a/b/c"},
{"a/b/c", "d/e/f", "a/b/c/d/e/f"},
{"a/b/c", "./d/e/f", "a/b/c/d/e/f"},
{"a/b/c", "../d/e/f", "a/b/c/d/e/f"},
{"a/b/c", "../../../etc/shadow", "a/b/c/etc/shadow"},
{"/a/b/c", "", "/a/b/c"},
{"/a/b/c", ".", "/a/b/c"},
{"/a/b/c", "d/e/f", "/a/b/c/d/e/f"},
{"/a/b/c", "./d/e/f", "/a/b/c/d/e/f"},
{"/a/b/c", "../d/e/f", "/a/b/c/d/e/f"},
{"/a/b/c", "../../../etc/shadow", "/a/b/c/etc/shadow"},
{"./a/b/c", "", "a/b/c"},
{"./a/b/c", ".", "a/b/c"},
{"./a/b/c", "d/e/f", "a/b/c/d/e/f"},
{"./a/b/c", "./d/e/f", "a/b/c/d/e/f"},
{"./a/b/c", "../d/e/f", "a/b/c/d/e/f"},
{"./a/b/c", "../../../etc/shadow", "a/b/c/etc/shadow"},
{"../a/b/c", "", "../a/b/c"},
{"../a/b/c", ".", "../a/b/c"},
{"../a/b/c", "d/e/f", "../a/b/c/d/e/f"},
{"../a/b/c", "./d/e/f", "../a/b/c/d/e/f"},
{"../a/b/c", "../d/e/f", "../a/b/c/d/e/f"},
{"../a/b/c", "../../../etc/shadow", "../a/b/c/etc/shadow"},
}
for _, c := range ts {
r := JoinPaths(c.trusted, c.untrusted)
if r != c.expected {
t.Fatalf("expected '%s', got '%s'. Inputs = '%s', '%s'", c.expected, r, c.trusted, c.untrusted)
}
}
}

View File

@@ -0,0 +1,120 @@
package filesystem
import "testing"
func TestJoinPaths(t *testing.T) {
var ts = []struct {
trusted string
untrusted string
expected string
}{
{"", "", "."},
{"", ".", "."},
{"", "d/e/f", `d\e\f`},
{"", "./d/e/f", `d\e\f`},
{"", "../d/e/f", `d\e\f`},
{"", "/d/e/f", `d\e\f`},
{"", "../../../windows/system.ini", `windows\system.ini`},
{"", `C:\windows\system.ini`, `windows\system.ini`},
{"", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
{"", `\\server\a\b\c`, `server\a\b\c`},
{"", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
{".", "", "."},
{".", ".", "."},
{".", "d/e/f", `d\e\f`},
{".", "./d/e/f", `d\e\f`},
{".", "../d/e/f", `d\e\f`},
{".", "/d/e/f", `d\e\f`},
{".", "../../../windows/system.ini", `windows\system.ini`},
{".", `C:\windows\system.ini`, `windows\system.ini`},
{".", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
{".", `\\server\a\b\c`, `server\a\b\c`},
{".", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
{"./", "", "."},
{"./", ".", "."},
{"./", "d/e/f", `d\e\f`},
{"./", "./d/e/f", `d\e\f`},
{"./", "../d/e/f", `d\e\f`},
{"./", "/d/e/f", `d\e\f`},
{"./", "../../../windows/system.ini", `windows\system.ini`},
{"./", `C:\windows\system.ini`, `windows\system.ini`},
{"./", `..\..\..\..\C:\windows\system.ini`, `windows\system.ini`},
{"./", `\\server\a\b\c`, `server\a\b\c`},
{"./", `..\..\..\..\\server\a\b\c`, `server\a\b\c`},
{"a/b/c", "", `a\b\c`},
{"a/b/c", ".", `a\b\c`},
{"a/b/c", "d/e/f", `a\b\c\d\e\f`},
{"a/b/c", "./d/e/f", `a\b\c\d\e\f`},
{"a/b/c", "../d/e/f", `a\b\c\d\e\f`},
{"a/b/c", "../../../windows/system.ini", `a\b\c\windows\system.ini`},
{"a/b/c", `C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
{"a/b/c", `..\..\..\..\C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
{"a/b/c", `\\server\a\b\c`, `a\b\c\server\a\b\c`},
{"a/b/c", `..\..\..\..\\server\a\b\c`, `a\b\c\server\a\b\c`},
{"/a/b/c", "", `\a\b\c`},
{"/a/b/c", ".", `\a\b\c`},
{"/a/b/c", "d/e/f", `\a\b\c\d\e\f`},
{"/a/b/c", "./d/e/f", `\a\b\c\d\e\f`},
{"/a/b/c", "../d/e/f", `\a\b\c\d\e\f`},
{"/a/b/c", "../../../windows/system.ini", `\a\b\c\windows\system.ini`},
{"/a/b/c", `C:\windows\system.ini`, `\a\b\c\C:\windows\system.ini`},
{"/a/b/c", `..\..\..\..\C:\windows\system.ini`, `\a\b\c\C:\windows\system.ini`},
{"/a/b/c", `\\server\a\b\c`, `\a\b\c\server\a\b\c`},
{"/a/b/c", `..\..\..\..\\server\a\b\c`, `\a\b\c\server\a\b\c`},
{"./a/b/c", "", `a\b\c`},
{"./a/b/c", ".", `a\b\c`},
{"./a/b/c", "d/e/f", `a\b\c\d\e\f`},
{"./a/b/c", "./d/e/f", `a\b\c\d\e\f`},
{"./a/b/c", "../d/e/f", `a\b\c\d\e\f`},
{"./a/b/c", "../../../windows/system.ini", `a\b\c\windows\system.ini`},
{"./a/b/c", `C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
{"./a/b/c", `..\..\..\..\C:\windows\system.ini`, `a\b\c\C:\windows\system.ini`},
{"./a/b/c", `\\server\a\b\c`, `a\b\c\server\a\b\c`},
{"./a/b/c", `..\..\..\..\\server\a\b\c`, `a\b\c\server\a\b\c`},
{"../a/b/c", "", `..\a\b\c`},
{"../a/b/c", ".", `..\a\b\c`},
{"../a/b/c", "d/e/f", `..\a\b\c\d\e\f`},
{"../a/b/c", "./d/e/f", `..\a\b\c\d\e\f`},
{"../a/b/c", "../d/e/f", `..\a\b\c\d\e\f`},
{"../a/b/c", "../../../windows/system.ini", `..\a\b\c\windows\system.ini`},
{"../a/b/c", `C:\windows\system.ini`, `..\a\b\c\C:\windows\system.ini`},
{"../a/b/c", `..\..\..\..\C:\windows\system.ini`, `..\a\b\c\C:\windows\system.ini`},
{"../a/b/c", `\\server\a\b\c`, `..\a\b\c\server\a\b\c`},
{"../a/b/c", `..\..\..\..\\server\a\b\c`, `..\a\b\c\server\a\b\c`},
{"C:/a/b/c", "", `C:\a\b\c`},
{"C:/a/b/c", ".", `C:\a\b\c`},
{"C:/a/b/c", "d/e/f", `C:\a\b\c\d\e\f`},
{"C:/a/b/c", "./d/e/f", `C:\a\b\c\d\e\f`},
{"C:/a/b/c", "../d/e/f", `C:\a\b\c\d\e\f`},
{"C:/a/b/c", "../../../windows/system.ini", `C:\a\b\c\windows\system.ini`},
{"C:/a/b/c", `C:\windows\system.ini`, `C:\a\b\c\C:\windows\system.ini`},
{"C:/a/b/c", `..\..\..\..\C:\windows\system.ini`, `C:\a\b\c\C:\windows\system.ini`},
{"C:/a/b/c", `\\server\a\b\c`, `C:\a\b\c\server\a\b\c`},
{"C:/a/b/c", `..\..\..\..\\server\a\b\c`, `C:\a\b\c\server\a\b\c`},
{`\\server\a\b\c`, "", `\\server\a\b\c`},
{`\\server\a\b\c`, ".", `\\server\a\b\c`},
{`\\server\a\b\c`, "d/e/f", `\\server\a\b\c\d\e\f`},
{`\\server\a\b\c`, "./d/e/f", `\\server\a\b\c\d\e\f`},
{`\\server\a\b\c`, "../d/e/f", `\\server\a\b\c\d\e\f`},
{`\\server\a\b\c`, "../../../windows/system.ini", `\\server\a\b\c\windows\system.ini`},
{`\\server\a\b\c`, `C:\windows\system.ini`, `\\server\a\b\c\C:\windows\system.ini`},
{`\\server\a\b\c`, `..\..\..\C:\windows\system.ini`, `\\server\a\b\c\C:\windows\system.ini`},
{`\\server\a\b\c`, `\\server\a\b\c`, `\\server\a\b\c\server\a\b\c`},
{`\\server\a\b\c`, `..\..\..\\server\a\b\c`, `\\server\a\b\c\server\a\b\c`},
}
for _, c := range ts {
r := JoinPaths(c.trusted, c.untrusted)
if r != c.expected {
t.Fatalf("expected '%s', got '%s'. Inputs = '%s', '%s'", c.expected, r, c.trusted, c.untrusted)
}
}
}

View File

@@ -3,58 +3,52 @@ module github.com/portainer/portainer/api
go 1.16
require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.4.16
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/Microsoft/go-winio v0.4.17
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/aws/aws-sdk-go-v2 v1.11.1
github.com/aws/aws-sdk-go-v2/credentials v1.6.2
github.com/aws/aws-sdk-go-v2/service/ecr v1.10.1
github.com/boltdb/bolt v1.3.1
github.com/containerd/containerd v1.3.1 // indirect
github.com/containerd/containerd v1.5.7 // indirect
github.com/coreos/go-semver v0.3.0
github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/docker/cli v0.0.0-20191126203649-54d085b857e9
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0
github.com/docker/cli v20.10.9+incompatible
github.com/docker/docker v20.10.9+incompatible
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
github.com/go-git/go-git/v5 v5.3.0
github.com/go-ldap/ldap/v3 v3.1.8
github.com/gofrs/uuid v3.2.0+incompatible
github.com/gofrs/uuid v4.0.0+incompatible
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.7.3
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/websocket v1.4.1
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/golang-lru v0.5.4
github.com/joho/godotenv v1.3.0
github.com/jpillora/chisel v0.0.0-20190724232113-f3a8df20e389
github.com/json-iterator/go v1.1.10
github.com/json-iterator/go v1.1.11
github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c
github.com/mattn/go-shellwords v1.0.6 // indirect
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6
github.com/pkg/errors v0.9.1
github.com/portainer/docker-compose-wrapper v0.0.0-20210909083948-8be0d98451a1
github.com/portainer/docker-compose-wrapper v0.0.0-20211018221743-10a04c9d4f19
github.com/portainer/libcrypto v0.0.0-20210422035235-c652195c5c3a
github.com/portainer/libhelm v0.0.0-20210929000907-825e93d62108
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
github.com/portainer/libhttp v0.0.0-20211021135806-13e6c55c5fbc
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
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
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gotest.tools v2.2.0+incompatible // indirect
k8s.io/api v0.17.2
k8s.io/apimachinery v0.17.2
k8s.io/client-go v0.17.2
k8s.io/api v0.22.2
k8s.io/apimachinery v0.22.2
k8s.io/client-go v0.22.2
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
)
replace github.com/docker/docker => github.com/docker/engine v1.4.2-0.20200204220554-5f6d6f3f2203
replace golang.org/x/sys => golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
package openamt
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
portainer "github.com/portainer/portainer/api"
)
type authenticationResponse struct {
Token string `json:"token"`
}
func (service *Service) executeAuthenticationRequest(configuration portainer.OpenAMTConfiguration) (*authenticationResponse, error) {
loginURL := fmt.Sprintf("https://%s/mps/login/api/v1/authorize", configuration.MPSServer)
payload := map[string]string{
"username": configuration.Credentials.MPSUser,
"password": configuration.Credentials.MPSPassword,
}
jsonValue, _ := json.Marshal(payload)
req, err := http.NewRequest(http.MethodPost, loginURL, bytes.NewBuffer(jsonValue))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
response, err := service.httpsClient.Do(req)
if err != nil {
return nil, err
}
responseBody, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
errorResponse := parseError(responseBody)
if errorResponse != nil {
return nil, errorResponse
}
var token authenticationResponse
err = json.Unmarshal(responseBody, &token)
if err != nil {
return nil, err
}
return &token, nil
}

View File

@@ -0,0 +1,143 @@
package openamt
import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
portainer "github.com/portainer/portainer/api"
)
type CIRAConfig struct {
ConfigName string `json:"configName"`
MPSServerAddress string `json:"mpsServerAddress"`
ServerAddressFormat int `json:"serverAddressFormat"`
CommonName string `json:"commonName"`
MPSPort int `json:"mpsPort"`
Username string `json:"username"`
MPSRootCertificate string `json:"mpsRootCertificate"`
RegeneratePassword bool `json:"regeneratePassword"`
AuthMethod int `json:"authMethod"`
}
func (service *Service) createOrUpdateCIRAConfig(configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
ciraConfig, err := service.getCIRAConfig(configuration, configName)
if err != nil {
return nil, err
}
method := http.MethodPost
if ciraConfig != nil {
method = http.MethodPatch
}
ciraConfig, err = service.saveCIRAConfig(method, configuration, configName)
if err != nil {
return nil, err
}
return ciraConfig, nil
}
func (service *Service) getCIRAConfig(configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
url := fmt.Sprintf("https://%s/rps/api/v1/admin/ciraconfigs/%s", configuration.MPSServer, configName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result CIRAConfig
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveCIRAConfig(method string, configuration portainer.OpenAMTConfiguration, configName string) (*CIRAConfig, error) {
url := fmt.Sprintf("https://%s/rps/api/v1/admin/ciraconfigs", configuration.MPSServer)
certificate, err := service.getCIRACertificate(configuration)
if err != nil {
return nil, err
}
addressFormat, err := addressFormat(configuration.MPSServer)
if err != nil {
return nil, err
}
config := CIRAConfig{
ConfigName: configName,
MPSServerAddress: configuration.MPSServer,
CommonName: configuration.MPSServer,
ServerAddressFormat: addressFormat,
MPSPort: 4433,
Username: "admin",
MPSRootCertificate: certificate,
RegeneratePassword: false,
AuthMethod: 2,
}
payload, _ := json.Marshal(config)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result CIRAConfig
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func addressFormat(url string) (int, error) {
ip := net.ParseIP(url)
if ip == nil {
return 201, nil // FQDN
}
if strings.Contains(url, ".") {
return 3, nil // IPV4
}
if strings.Contains(url, ":") {
return 4, nil // IPV6
}
return 0, fmt.Errorf("could not determine server address format for %s", url)
}
func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfiguration) (string, error) {
loginURL := fmt.Sprintf("https://%s/mps/api/v1/ciracert", configuration.MPSServer)
req, err := http.NewRequest(http.MethodGet, loginURL, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", configuration.Credentials.MPSToken))
response, err := service.httpsClient.Do(req)
if err != nil {
return "", err
}
if response.StatusCode != http.StatusOK {
return "", errors.New(fmt.Sprintf("unexpected status code %s", response.Status))
}
certificate, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
block, _ := pem.Decode(certificate)
return base64.StdEncoding.EncodeToString(block.Bytes), nil
}

View File

@@ -0,0 +1,81 @@
package openamt
import (
"encoding/json"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
)
type (
Domain struct {
DomainName string `json:"profileName"`
DomainSuffix string `json:"domainSuffix"`
ProvisioningCert string `json:"provisioningCert"`
ProvisioningCertPassword string `json:"provisioningCertPassword"`
ProvisioningCertStorageFormat string `json:"provisioningCertStorageFormat"`
}
)
func (service *Service) createOrUpdateDomain(configuration portainer.OpenAMTConfiguration) (*Domain, error) {
domain, err := service.getDomain(configuration)
if err != nil {
return nil, err
}
method := http.MethodPost
if domain != nil {
method = http.MethodPatch
}
domain, err = service.saveDomain(method, configuration)
if err != nil {
return nil, err
}
return domain, nil
}
func (service *Service) getDomain(configuration portainer.OpenAMTConfiguration) (*Domain, error) {
url := fmt.Sprintf("https://%s/rps/api/v1/admin/domains/%s", configuration.MPSServer, configuration.DomainConfiguration.DomainName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result Domain
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveDomain(method string, configuration portainer.OpenAMTConfiguration) (*Domain, error) {
url := fmt.Sprintf("https://%s/rps/api/v1/admin/domains", configuration.MPSServer)
profile := Domain{
DomainName: configuration.DomainConfiguration.DomainName,
DomainSuffix: configuration.DomainConfiguration.DomainName,
ProvisioningCert: configuration.DomainConfiguration.CertFileText,
ProvisioningCertPassword: configuration.DomainConfiguration.CertPassword,
ProvisioningCertStorageFormat: "string",
}
payload, _ := json.Marshal(profile)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result Domain
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -0,0 +1,104 @@
package openamt
import (
"encoding/json"
"fmt"
"net/http"
portainer "github.com/portainer/portainer/api"
)
type (
Profile struct {
ProfileName string `json:"profileName"`
Activation string `json:"activation"`
CIRAConfigName *string `json:"ciraConfigName"`
GenerateRandomAMTPassword bool `json:"generateRandomPassword"`
AMTPassword string `json:"amtPassword"`
GenerateRandomMEBxPassword bool `json:"generateRandomMEBxPassword"`
MEBXPassword string `json:"mebxPassword"`
Tags []string `json:"tags"`
DHCPEnabled bool `json:"dhcpEnabled"`
TenantId string `json:"tenantId"`
WIFIConfigs []ProfileWifiConfig `json:"wifiConfigs"`
}
ProfileWifiConfig struct {
Priority int `json:"priority"`
ProfileName string `json:"profileName"`
}
)
func (service *Service) createOrUpdateAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
profile, err := service.getAMTProfile(configuration, profileName)
if err != nil {
return nil, err
}
method := http.MethodPost
if profile != nil {
method = http.MethodPatch
}
profile, err = service.saveAMTProfile(method, configuration, profileName, ciraConfigName, wirelessConfig)
if err != nil {
return nil, err
}
return profile, nil
}
func (service *Service) getAMTProfile(configuration portainer.OpenAMTConfiguration, profileName string) (*Profile, error) {
url := fmt.Sprintf("https://%s/rps/api/v1/admin/profiles/%s", configuration.MPSServer, profileName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result Profile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveAMTProfile(method string, configuration portainer.OpenAMTConfiguration, profileName string, ciraConfigName string, wirelessConfig string) (*Profile, error) {
url := fmt.Sprintf("https://%s/rps/api/v1/admin/profiles", configuration.MPSServer)
profile := Profile{
ProfileName: profileName,
Activation: "acmactivate",
GenerateRandomAMTPassword: false,
GenerateRandomMEBxPassword: false,
AMTPassword: configuration.Credentials.MPSPassword,
MEBXPassword: configuration.Credentials.MPSPassword,
CIRAConfigName: &ciraConfigName,
Tags: []string{},
DHCPEnabled: true,
}
if wirelessConfig != "" {
profile.WIFIConfigs = []ProfileWifiConfig{
{
Priority: 1,
ProfileName: DefaultWirelessConfigName,
},
}
}
payload, _ := json.Marshal(profile)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result Profile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -0,0 +1,91 @@
package openamt
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
portainer "github.com/portainer/portainer/api"
)
type (
WirelessProfile struct {
ProfileName string `json:"profileName"`
AuthenticationMethod int `json:"authenticationMethod"`
EncryptionMethod int `json:"encryptionMethod"`
SSID string `json:"ssid"`
PSKPassphrase string `json:"pskPassphrase"`
}
)
func (service *Service) createOrUpdateWirelessConfig(configuration portainer.OpenAMTConfiguration, wirelessConfigName string) (*WirelessProfile, error) {
wirelessConfig, err := service.getWirelessConfig(configuration, wirelessConfigName)
if err != nil {
return nil, err
}
method := http.MethodPost
if wirelessConfig != nil {
method = http.MethodPatch
}
wirelessConfig, err = service.saveWirelessConfig(method, configuration, wirelessConfigName)
if err != nil {
return nil, err
}
return wirelessConfig, nil
}
func (service *Service) getWirelessConfig(configuration portainer.OpenAMTConfiguration, configName string) (*WirelessProfile, error) {
url := fmt.Sprintf("https://%s/rps/api/v1/admin/wirelessconfigs/%s", configuration.MPSServer, configName)
responseBody, err := service.executeGetRequest(url, configuration.Credentials.MPSToken)
if err != nil {
return nil, err
}
if responseBody == nil {
return nil, nil
}
var result WirelessProfile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func (service *Service) saveWirelessConfig(method string, configuration portainer.OpenAMTConfiguration, configName string) (*WirelessProfile, error) {
parsedAuthenticationMethod, err := strconv.Atoi(configuration.WirelessConfiguration.AuthenticationMethod)
if err != nil {
return nil, fmt.Errorf("error parsing wireless authentication method: %s", err.Error())
}
parsedEncryptionMethod, err := strconv.Atoi(configuration.WirelessConfiguration.EncryptionMethod)
if err != nil {
return nil, fmt.Errorf("error parsing wireless encryption method: %s", err.Error())
}
url := fmt.Sprintf("https://%s/rps/api/v1/admin/wirelessconfigs", configuration.MPSServer)
config := WirelessProfile{
ProfileName: configName,
AuthenticationMethod: parsedAuthenticationMethod,
EncryptionMethod: parsedEncryptionMethod,
SSID: configuration.WirelessConfiguration.SSID,
PSKPassphrase: configuration.WirelessConfiguration.PskPass,
}
payload, _ := json.Marshal(config)
responseBody, err := service.executeSaveRequest(method, url, configuration.Credentials.MPSToken, payload)
if err != nil {
return nil, err
}
var result WirelessProfile
err = json.Unmarshal(responseBody, &result)
if err != nil {
return nil, err
}
return &result, nil
}

View File

@@ -0,0 +1,157 @@
package openamt
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"time"
portainer "github.com/portainer/portainer/api"
)
const (
DefaultCIRAConfigName = "ciraConfigDefault"
DefaultWirelessConfigName = "wirelessProfileDefault"
DefaultProfileName = "profileAMTDefault"
)
// Service represents a service for managing an OpenAMT server.
type Service struct {
httpsClient *http.Client
}
// NewService initializes a new service.
func NewService(dataStore portainer.DataStore) *Service {
if !dataStore.Settings().IsFeatureFlagEnabled(portainer.FeatOpenAMT) {
return nil
}
return &Service{
httpsClient: &http.Client{
Timeout: time.Second * time.Duration(5),
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
},
}
}
type openAMTError struct {
ErrorMsg string `json:"message"`
Errors []struct {
ErrorMsg string `json:"msg"`
} `json:"errors"`
}
func parseError(responseBody []byte) error {
var errorResponse openAMTError
err := json.Unmarshal(responseBody, &errorResponse)
if err != nil {
return err
}
if len(errorResponse.Errors) > 0 {
return errors.New(errorResponse.Errors[0].ErrorMsg)
}
if errorResponse.ErrorMsg != "" {
return errors.New(errorResponse.ErrorMsg)
}
return nil
}
func (service *Service) ConfigureDefault(configuration portainer.OpenAMTConfiguration) error {
token, err := service.executeAuthenticationRequest(configuration)
if err != nil {
return err
}
configuration.Credentials.MPSToken = token.Token
ciraConfig, err := service.createOrUpdateCIRAConfig(configuration, DefaultCIRAConfigName)
if err != nil {
return err
}
wirelessConfigName := ""
if configuration.WirelessConfiguration != nil {
wirelessConfig, err := service.createOrUpdateWirelessConfig(configuration, DefaultWirelessConfigName)
if err != nil {
return err
}
wirelessConfigName = wirelessConfig.ProfileName
}
_, err = service.createOrUpdateAMTProfile(configuration, DefaultProfileName, ciraConfig.ConfigName, wirelessConfigName)
if err != nil {
return err
}
_, err = service.createOrUpdateDomain(configuration)
if err != nil {
return err
}
return nil
}
func (service *Service) executeSaveRequest(method string, url string, token string, payload []byte) ([]byte, error) {
req, err := http.NewRequest(method, url, bytes.NewBuffer(payload))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
response, err := service.httpsClient.Do(req)
if err != nil {
return nil, err
}
responseBody, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
if response.StatusCode < 200 || response.StatusCode > 300 {
errorResponse := parseError(responseBody)
if errorResponse != nil {
return nil, errorResponse
}
return nil, errors.New(fmt.Sprintf("unexpected status code %s", response.Status))
}
return responseBody, nil
}
func (service *Service) executeGetRequest(url string, token string) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
response, err := service.httpsClient.Do(req)
if err != nil {
return nil, err
}
responseBody, readErr := ioutil.ReadAll(response.Body)
if readErr != nil {
return nil, readErr
}
if response.StatusCode < 200 || response.StatusCode > 300 {
if response.StatusCode == http.StatusNotFound {
return nil, nil
}
errorResponse := parseError(responseBody)
if errorResponse != nil {
return nil, errorResponse
}
return nil, errors.New(fmt.Sprintf("unexpected status code %s", response.Status))
}
return responseBody, nil
}

View File

@@ -39,6 +39,7 @@ func (payload *authenticatePayload) Validate(r *http.Request) error {
// @id AuthenticateUser
// @summary Authenticate
// @description **Access policy**: public
// @description Use this environment(endpoint) to authenticate against Portainer using a username and password.
// @tags auth
// @accept json

View File

@@ -44,6 +44,7 @@ func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuth
// @id ValidateOAuth
// @summary Authenticate with OAuth
// @description **Access policy**: public
// @tags auth
// @accept json
// @produce json

View File

@@ -10,6 +10,8 @@ import (
// @id Logout
// @summary Logout
// @description **Access policy**: authenticated
// @security ApiKeyAuth
// @security jwt
// @tags auth
// @success 204 "Success"

View File

@@ -26,9 +26,11 @@ func (p *backupPayload) Validate(r *http.Request) error {
// @description Creates an archive with a system data snapshot that could be used to restore the system.
// @description **Access policy**: admin
// @tags backup
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce octet-stream
// @param Password body string false "Password to encrypt the backup with"
// @param body body backupPayload false "An object contains the password to encrypt the backup with"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"

View File

@@ -22,10 +22,9 @@ type restorePayload struct {
// @description Triggers a system restore using provided backup file
// @description **Access policy**: public
// @tags backup
// @param FileContent body []byte true "Content of the backup"
// @param FileName body string true "File name"
// @param Password body string false "Password to decrypt the backup with"
// @success 200 "Success"
// @accept json
// @param restorePayload body restorePayload true "Restore request payload"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 500 "Server error"
// @router /restore [post]

View File

@@ -2,6 +2,7 @@ package customtemplates
import (
"errors"
"log"
"net/http"
"regexp"
"strconv"
@@ -21,6 +22,7 @@ import (
// @description Create a custom template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @accept json,multipart/form-data
// @produce json
@@ -270,6 +272,23 @@ func (handler *Handler) createCustomTemplateFromGitRepository(r *http.Request) (
return nil, err
}
entryPath := filesystem.JoinPaths(projectPath, customTemplate.EntryPoint)
exists, err := handler.FileService.FileExists(entryPath)
if err != nil || !exists {
if err := handler.FileService.RemoveDirectory(projectPath); err != nil {
log.Printf("[WARN] [http,customtemplate,git] [error: %s] [message: unable to remove git repository directory]", err)
}
}
if err != nil {
return nil, err
}
if !exists {
return nil, errors.New("Invalid Compose file, ensure that the Compose file path is correct")
}
return customTemplate, nil
}

View File

@@ -16,8 +16,9 @@ import (
// @id CustomTemplateDelete
// @summary Remove a template
// @description Remove a template.
// @description **Access policy**: authorized
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @param id path int true "Template identifier"
// @success 204 "Success"

View File

@@ -2,7 +2,6 @@ package customtemplates
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -18,8 +17,9 @@ type fileResponse struct {
// @id CustomTemplateFile
// @summary Get Template stack file content.
// @description Retrieve the content of the Stack file for the specified custom template
// @description **Access policy**: authorized
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param id path int true "Template identifier"
@@ -41,7 +41,7 @@ func (handler *Handler) customTemplateFile(w http.ResponseWriter, r *http.Reques
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a custom template with the specified identifier inside the database", err}
}
fileContent, err := handler.FileService.GetFileContent(path.Join(customTemplate.ProjectPath, customTemplate.EntryPoint))
fileContent, err := handler.FileService.GetFileContent(customTemplate.ProjectPath, customTemplate.EntryPoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom template file from disk", err}
}

View File

@@ -18,8 +18,8 @@ import (
// @description Retrieve details about a template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "Template identifier"
// @success 200 {object} portainer.CustomTemplate "Success"

View File

@@ -17,6 +17,7 @@ import (
// @description List available custom templates.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @produce json
// @param type query []int true "Template types" Enums(1,2,3)

View File

@@ -62,6 +62,7 @@ func (payload *customTemplateUpdatePayload) Validate(r *http.Request) error {
// @description Update a template.
// @description **Access policy**: authenticated
// @tags custom_templates
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@@ -34,8 +34,9 @@ func (payload *edgeGroupCreatePayload) Validate(r *http.Request) error {
// @id EdgeGroupCreate
// @summary Create an EdgeGroup
// @description
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@@ -13,11 +13,10 @@ import (
// @id EdgeGroupDelete
// @summary Deletes an EdgeGroup
// @description
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "EdgeGroup Id"
// @success 204
// @failure 503 "Edge compute features are disabled"

View File

@@ -12,10 +12,10 @@ import (
// @id EdgeGroupInspect
// @summary Inspects an EdgeGroup
// @description
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path int true "EdgeGroup Id"
// @success 200 {object} portainer.EdgeGroup

View File

@@ -17,12 +17,12 @@ type decoratedEdgeGroup struct {
// @id EdgeGroupList
// @summary list EdgeGroups
// @description
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @success 200 {array} portainer.EdgeGroup{HasEdgeStack=bool} "EdgeGroups"
// @success 200 {array} decoratedEdgeGroup "EdgeGroups"
// @failure 500
// @failure 503 "Edge compute features are disabled"
// @router /edge_groups [get]

View File

@@ -36,8 +36,9 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
// @id EgeGroupUpdate
// @summary Updates an EdgeGroup
// @description
// @description **Access policy**: administrator
// @tags edge_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@@ -16,10 +16,10 @@ import (
// @id EdgeJobCreate
// @summary Create an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param method query string true "Creation Method" Enums(file, string)
// @param body_string body edgeJobCreateFromFileContentPayload true "EdgeGroup data when method is string"

View File

@@ -13,11 +13,10 @@ import (
// @id EdgeJobDelete
// @summary Delete an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @success 204
// @failure 500

View File

@@ -16,10 +16,10 @@ type edgeJobFileResponse struct {
// @id EdgeJobFile
// @summary Fetch a file of an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @success 200 {object} edgeJobFileResponse
@@ -40,7 +40,7 @@ func (handler *Handler) edgeJobFile(w http.ResponseWriter, r *http.Request) *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an Edge job with the specified identifier inside the database", err}
}
edgeJobFileContent, err := handler.FileService.GetFileContent(edgeJob.ScriptPath)
edgeJobFileContent, err := handler.FileService.GetFileContent("", edgeJob.ScriptPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file from disk", err}
}

View File

@@ -17,10 +17,10 @@ type edgeJobInspectResponse struct {
// @id EdgeJobInspect
// @summary Inspect an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @success 200 {object} portainer.EdgeJob

View File

@@ -9,10 +9,10 @@ import (
// @id EdgeJobList
// @summary Fetch EdgeJobs list
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @success 200 {array} portainer.EdgeJob
// @failure 500

View File

@@ -13,10 +13,10 @@ import (
// @id EdgeJobTasksClear
// @summary Clear the log for a specifc task on an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @param taskID path string true "Task Id"

View File

@@ -12,10 +12,10 @@ import (
// @id EdgeJobTasksCollect
// @summary Collect the log for a specifc task on an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @param taskID path string true "Task Id"

View File

@@ -15,10 +15,10 @@ type fileResponse struct {
// @id EdgeJobTaskLogsInspect
// @summary Fetch the log for a specifc task on an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @param taskID path string true "Task Id"

View File

@@ -19,10 +19,10 @@ type taskContainer struct {
// @id EdgeJobTasksList
// @summary Fetch the list of tasks on an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @success 200 {array} taskContainer

View File

@@ -30,8 +30,9 @@ func (payload *edgeJobUpdatePayload) Validate(r *http.Request) error {
// @id EdgeJobUpdate
// @summary Update an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@@ -19,10 +19,10 @@ import (
// @id EdgeStackCreate
// @summary Create an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param method query string true "Creation Method" Enums(file,string,repository)
// @param body_string body swarmStackFromFileContentPayload true "Required when using method=string"

View File

@@ -13,11 +13,10 @@ import (
// @id EdgeStackDelete
// @summary Delete an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeStack Id"
// @success 204
// @failure 500

View File

@@ -2,7 +2,6 @@ package edgestacks
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -17,10 +16,10 @@ type stackFileResponse struct {
// @id EdgeStackFile
// @summary Fetches the stack file for an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeStack Id"
// @success 200 {object} stackFileResponse
@@ -46,7 +45,7 @@ func (handler *Handler) edgeStackFile(w http.ResponseWriter, r *http.Request) *h
fileName = stack.ManifestPath
}
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, fileName))
stackFileContent, err := handler.FileService.GetFileContent(stack.ProjectPath, fileName)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
}

View File

@@ -12,10 +12,10 @@ import (
// @id EdgeStackInspect
// @summary Inspect an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeStack Id"
// @success 200 {object} portainer.EdgeStack

View File

@@ -9,10 +9,10 @@ import (
// @id EdgeStackList
// @summary Fetches the list of EdgeStacks
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json
// @success 200 {array} portainer.EdgeStack
// @failure 500

View File

@@ -33,8 +33,9 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
// @id EdgeStackUpdate
// @summary Update an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@@ -3,7 +3,6 @@ package edgestacks
import (
"fmt"
"net/http"
"path"
"strconv"
"github.com/gorilla/mux"
@@ -56,7 +55,7 @@ func (handler *Handler) convertAndStoreKubeManifestIfNeeded(edgeStack *portainer
return nil
}
composeConfig, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, edgeStack.EntryPoint))
composeConfig, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, edgeStack.EntryPoint)
if err != nil {
return fmt.Errorf("unable to retrieve Compose file from disk: %w", err)
}

View File

@@ -17,8 +17,9 @@ type templateFileFormat struct {
// @id EdgeTemplateList
// @summary Fetches the list of Edge Templates
// @description
// @description **Access policy**: administrator
// @tags edge_templates
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

View File

@@ -21,7 +21,7 @@ func (payload *logsPayload) Validate(r *http.Request) error {
// endpointEdgeJobsLogs
// @summary Inspect an EdgeJob Log
// @description
// @description **Access policy**: public
// @tags edge, endpoints
// @accept json
// @produce json

View File

@@ -3,7 +3,6 @@ package endpointedge
import (
"errors"
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -19,7 +18,7 @@ type configResponse struct {
}
// @summary Inspect an Edge Stack for an Environment(Endpoint)
// @description
// @description **Access policy**: public
// @tags edge, endpoints, edge_stacks
// @accept json
// @produce json
@@ -75,7 +74,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
}
}
stackFileContent, err := handler.FileService.GetFileContent(path.Join(edgeStack.ProjectPath, fileName))
stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Compose file from disk", err}
}

View File

@@ -36,6 +36,7 @@ func (payload *endpointGroupCreatePayload) Validate(r *http.Request) error {
// @description Create a new environment(endpoint) group.
// @description **Access policy**: administrator
// @tags endpoint_groups
// @security ApiKeyAuth
// @security jwt
// @accept json
// @produce json

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