Compare commits

..

101 Commits

Author SHA1 Message Date
Hao Zhang
cd786c5041 fix(logs): logs copy issues 2021-11-23 17:34:11 +08:00
Hao Zhang
16e5ba3f88 Merge branch 'develop' into fix/EE-1973/container-logs-download-extra-CR 2021-11-23 17:27:20 +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
Hao Zhang
606fe54321 use the same solution with EE 2021-11-15 10:03:47 +08: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
Hao Zhang
f2113a515b fix(frontend): use regex rather than chars to replace extra CRs [EE-1973] (#5312) 2021-11-08 23:02:45 +08:00
Richard Wei
d0ecbc49c8 fix(frontend): use regex rather than chars to replace extra CRs EE-1973 (#5312) 2021-11-08 22:59:37 +08:00
阿帕奇
dee1428bfb [#5312]fix extra CRs in downloading container logs 2021-11-08 16:58:08 +08: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
378 changed files with 16191 additions and 4284 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

@@ -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

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

@@ -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(),

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,8 +2,10 @@ package main
import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
portainer "github.com/portainer/portainer/api"
@@ -237,6 +239,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 {
@@ -467,6 +512,8 @@ 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)
@@ -490,6 +537,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
}
}
err = enableFeaturesFromFlags(dataStore, flags)
if err != nil {
log.Fatalf("failed enabling feature flag: %v", err)
}
err = edge.LoadEdgeJobs(dataStore, reverseTunnelService)
if err != nil {
log.Fatalf("failed loading edge jobs from database: %v", err)
@@ -504,7 +556,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)
}

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

@@ -44,19 +44,26 @@ 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)
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 +71,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 +94,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 +121,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 +134,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 +157,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
}
}
return command, args
return command, args, nil
}
func (manager *SwarmStackManager) updateDockerCLIConfiguration(configPath string) error {
@@ -175,7 +191,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,47 @@ 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/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/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
)
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

@@ -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,7 @@ import (
// @id Logout
// @summary Logout
// @description **Access policy**: authenticated
// @security jwt
// @tags auth
// @success 204 "Success"

View File

@@ -27,8 +27,9 @@ func (p *backupPayload) Validate(r *http.Request) error {
// @description **Access policy**: admin
// @tags backup
// @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"
@@ -270,6 +271,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,7 +16,7 @@ import (
// @id CustomTemplateDelete
// @summary Remove a template
// @description Remove a template.
// @description **Access policy**: authorized
// @description **Access policy**: authenticated
// @tags custom_templates
// @security jwt
// @param id path int true "Template identifier"

View File

@@ -2,7 +2,6 @@ package customtemplates
import (
"net/http"
"path"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
@@ -18,7 +17,7 @@ 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 jwt
// @produce json
@@ -41,7 +40,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

@@ -19,7 +19,6 @@ import (
// @description **Access policy**: authenticated
// @tags custom_templates
// @security jwt
// @accept json
// @produce json
// @param id path int true "Template identifier"
// @success 200 {object} portainer.CustomTemplate "Success"

View File

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

View File

@@ -13,11 +13,9 @@ import (
// @id EdgeGroupDelete
// @summary Deletes an EdgeGroup
// @description
// @description **Access policy**: administrator
// @tags edge_groups
// @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,9 @@ import (
// @id EdgeGroupInspect
// @summary Inspects an EdgeGroup
// @description
// @description **Access policy**: administrator
// @tags edge_groups
// @security jwt
// @accept json
// @produce json
// @param id path int true "EdgeGroup Id"
// @success 200 {object} portainer.EdgeGroup

View File

@@ -17,12 +17,11 @@ type decoratedEdgeGroup struct {
// @id EdgeGroupList
// @summary list EdgeGroups
// @description
// @description **Access policy**: administrator
// @tags edge_groups
// @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,7 +36,7 @@ func (payload *edgeGroupUpdatePayload) Validate(r *http.Request) error {
// @id EgeGroupUpdate
// @summary Updates an EdgeGroup
// @description
// @description **Access policy**: administrator
// @tags edge_groups
// @security jwt
// @accept json

View File

@@ -16,10 +16,9 @@ import (
// @id EdgeJobCreate
// @summary Create an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @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,9 @@ import (
// @id EdgeJobDelete
// @summary Delete an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @success 204
// @failure 500

View File

@@ -16,10 +16,9 @@ type edgeJobFileResponse struct {
// @id EdgeJobFile
// @summary Fetch a file of an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @success 200 {object} edgeJobFileResponse
@@ -40,7 +39,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,9 @@ type edgeJobInspectResponse struct {
// @id EdgeJobInspect
// @summary Inspect an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @success 200 {object} portainer.EdgeJob

View File

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

View File

@@ -13,10 +13,9 @@ import (
// @id EdgeJobTasksClear
// @summary Clear the log for a specifc task on an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @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,9 @@ import (
// @id EdgeJobTasksCollect
// @summary Collect the log for a specifc task on an EdgeJob
// @description
// @description **Access policy**: administrator
// @tags edge_jobs
// @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,9 @@ 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 jwt
// @accept json
// @produce json
// @param id path string true "EdgeJob Id"
// @param taskID path string true "Task Id"

View File

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

View File

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

View File

@@ -19,10 +19,9 @@ import (
// @id EdgeStackCreate
// @summary Create an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @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,9 @@ import (
// @id EdgeStackDelete
// @summary Delete an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @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,9 @@ type stackFileResponse struct {
// @id EdgeStackFile
// @summary Fetches the stack file for an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeStack Id"
// @success 200 {object} stackFileResponse
@@ -46,7 +44,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,9 @@ import (
// @id EdgeStackInspect
// @summary Inspect an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @security jwt
// @accept json
// @produce json
// @param id path string true "EdgeStack Id"
// @success 200 {object} portainer.EdgeStack

View File

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

View File

@@ -33,7 +33,7 @@ func (payload *updateEdgeStackPayload) Validate(r *http.Request) error {
// @id EdgeStackUpdate
// @summary Update an EdgeStack
// @description
// @description **Access policy**: administrator
// @tags edge_stacks
// @security jwt
// @accept 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,7 +17,7 @@ type templateFileFormat struct {
// @id EdgeTemplateList
// @summary Fetches the list of Edge Templates
// @description
// @description **Access policy**: administrator
// @tags edge_templates
// @security jwt
// @accept 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

@@ -17,8 +17,6 @@ import (
// @description **Access policy**: administrator
// @tags endpoint_groups
// @security jwt
// @accept json
// @produce json
// @param id path int true "EndpointGroup identifier"
// @success 204 "Success"
// @failure 400 "Invalid request"

View File

@@ -2,14 +2,12 @@ package endpointproxy
import (
"errors"
"strconv"
"strings"
"time"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"strconv"
"strings"
"net/http"
)
@@ -37,22 +35,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
}
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
handler.ProxyManager.DeleteEndpointProxy(endpoint)
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get the active tunnel", err}
}
}

View File

@@ -3,13 +3,11 @@ package endpointproxy
import (
"errors"
"fmt"
"strings"
"time"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"strings"
"net/http"
)
@@ -37,22 +35,9 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the environment", errors.New("No agent available")}
}
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
if tunnel.Status == portainer.EdgeAgentIdle {
handler.ProxyManager.DeleteEndpointProxy(endpoint)
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
}
settings, err := handler.DataStore.Settings().Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
time.Sleep(waitForAgentToConnect * 2)
_, err := handler.ReverseTunnelService.GetActiveTunnel(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get the active tunnel", err}
}
}

View File

@@ -27,7 +27,7 @@ import (
// @failure 400 "Invalid request"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @router /api/endpoints/{id}/association [put]
// @router /endpoints/{id}/association [put]
func (handler *Handler) endpointAssociationDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -49,7 +49,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove environment from the database", err}
}
handler.ProxyManager.DeleteEndpointProxy(endpoint)
handler.ProxyManager.DeleteEndpointProxy(endpoint.ID)
err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID)
if err != nil {

View File

@@ -18,11 +18,27 @@ import (
)
type dockerhubStatusResponse struct {
// Remaiming images to pull
Remaining int `json:"remaining"`
Limit int `json:"limit"`
// Daily limit
Limit int `json:"limit"`
}
// GET request on /api/endpoints/{id}/dockerhub/{registryId}
// @id endpointDockerhubStatus
// @summary fetch docker pull limits
// @description get docker pull limits for a docker hub registry in the environment
// @description **Access policy**:
// @tags endpoints
// @security jwt
// @produce json
// @param id path int true "endpoint ID"
// @param registryId path int true "registry ID"
// @success 200 {object} dockerhubStatusResponse "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "registry or endpoint not found"
// @failure 500 "Server error"
// @router /endpoints/{id}/dockerhub/{registryId} [get]
func (handler *Handler) endpointDockerhubStatus(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -29,6 +29,12 @@ func (payload *endpointExtensionAddPayload) Validate(r *http.Request) error {
return nil
}
// @id endpointExtensionAdd
// @tags endpoints
// @deprecated
// @param id path int true "Environment(Endpoint) identifier"
// @success 204 "Success"
// @router /endpoints/{id}/extensions [post]
func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -12,6 +12,13 @@ import (
"github.com/portainer/portainer/api/bolt/errors"
)
// @id endpointExtensionRemove
// @tags endpoints
// @deprecated
// @param id path int true "Environment(Endpoint) identifier"
// @param extensionType path string true "Extension Type"
// @success 204 "Success"
// @router /endpoints/{id}/extensions/{extensionType} [delete]
func (handler *Handler) endpointExtensionRemove(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -12,7 +12,20 @@ import (
"github.com/portainer/portainer/api/http/security"
)
// GET request on /endpoints/{id}/registries/{registryId}
// @id endpointRegistryInspect
// @summary get registry for environment
// @description **Access policy**: authenticated
// @tags endpoints
// @security jwt
// @produce json
// @param id path int true "identifier"
// @param registryId path int true "Registry identifier"
// @success 200 {object} portainer.Registry "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Registry not found"
// @failure 500 "Server error"
// @router /endpoints/{id}/registries/{registryId} [get]
func (handler *Handler) endpointRegistryInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -13,7 +13,18 @@ import (
"github.com/portainer/portainer/api/internal/endpointutils"
)
// GET request on /endpoints/{id}/registries?namespace
// @id endpointRegistriesList
// @summary List Registries on environment
// @description List all registries based on the current user authorizations in current environment.
// @description **Access policy**: authenticated
// @tags endpoints
// @param namespace query string false "required if kubernetes environment, will show registries by namespace"
// @security jwt
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @success 200 {array} portainer.Registry "Success"
// @failure 500 "Server error"
// @router /endpoints/{id}/registries [get]
func (handler *Handler) endpointRegistriesList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {

View File

@@ -21,7 +21,22 @@ func (payload *registryAccessPayload) Validate(r *http.Request) error {
return nil
}
// PUT request on /endpoints/{id}/registries/{registryId}
// @id endpointRegistryAccess
// @summary update registry access for environment
// @description **Access policy**: authenticated
// @tags endpoints
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param registryId path int true "Registry identifier"
// @param body body registryAccessPayload true "details"
// @success 204 "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Endpoint not found"
// @failure 500 "Server error"
// @router /endpoints/{id}/registries/{registryId} [put]
func (handler *Handler) endpointRegistryAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -36,9 +36,9 @@ func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
}
// @id EndpointSettingsUpdate
// @summary Update settings for an environments(endpoints)
// @description Update settings for an environments(endpoints).
// @description **Access policy**: administrator
// @summary Update settings for an environment(endpoint)
// @description Update settings for an environment(endpoint).
// @description **Access policy**: authenticated
// @security jwt
// @tags endpoints
// @accept json
@@ -49,7 +49,7 @@ func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
// @failure 400 "Invalid request"
// @failure 404 "Environment(Endpoint) not found"
// @failure 500 "Server error"
// @router /api/endpoints/{id}/settings [put]
// @router /endpoints/{id}/settings [put]
func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

View File

@@ -12,9 +12,9 @@ import (
)
// @id EndpointSnapshot
// @summary Snapshots an environments(endpoints)
// @description Snapshots an environments(endpoints)
// @description **Access policy**: restricted
// @summary Snapshots an environment(endpoint)
// @description Snapshots an environment(endpoint)
// @description **Access policy**: administrator
// @tags endpoints
// @security jwt
// @param id path int true "Environment(Endpoint) identifier"

View File

@@ -131,7 +131,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
Version: job.Version,
}
file, err := handler.FileService.GetFileContent(job.ScriptPath)
file, err := handler.FileService.GetFileContent("", job.ScriptPath)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve Edge job script file", err}

View File

@@ -56,7 +56,7 @@ func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
// @id EndpointUpdate
// @summary Update an environment(endpoint)
// @description Update an environment(endpoint).
// @description **Access policy**: administrator
// @description **Access policy**: authenticated
// @security jwt
// @tags endpoints
// @accept json

View File

@@ -60,7 +60,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/endpoints/{id}",
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/dockerhub/{registryId}",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointDockerhubStatus))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}/extensions",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost)
h.Handle("/endpoints/{id}/extensions/{extensionType}",

View File

@@ -27,6 +27,7 @@ import (
"github.com/portainer/portainer/api/http/handler/ssl"
"github.com/portainer/portainer/api/http/handler/stacks"
"github.com/portainer/portainer/api/http/handler/status"
"github.com/portainer/portainer/api/http/handler/storybook"
"github.com/portainer/portainer/api/http/handler/tags"
"github.com/portainer/portainer/api/http/handler/teammemberships"
"github.com/portainer/portainer/api/http/handler/teams"
@@ -63,6 +64,7 @@ type Handler struct {
SSLHandler *ssl.Handler
StackHandler *stacks.Handler
StatusHandler *status.Handler
StorybookHandler *storybook.Handler
TagHandler *tags.Handler
TeamMembershipHandler *teammemberships.Handler
TeamHandler *teams.Handler
@@ -227,6 +229,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/webhooks"):
http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/storybook"):
http.StripPrefix("/storybook", h.StorybookHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/"):
h.FileHandler.ServeHTTP(w, r)
}

View File

@@ -12,11 +12,9 @@ import (
// @id HelmDelete
// @summary Delete Helm Release
// @description
// @description **Access policy**: authorized
// @description **Access policy**: authenticated
// @tags helm
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param release path string true "The name of the release/application to uninstall"
// @param namespace query string true "An optional namespace"

View File

@@ -36,7 +36,7 @@ var errChartNameInvalid = errors.New("invalid chart name. " +
// @id HelmInstall
// @summary Install Helm Chart
// @description
// @description **Access policy**: authorized
// @description **Access policy**: authenticated
// @tags helm
// @security jwt
// @accept json

View File

@@ -12,7 +12,7 @@ import (
// @id HelmList
// @summary List Helm Releases
// @description
// @description **Access policy**: authorized
// @description **Access policy**: authenticated
// @tags helm
// @security jwt
// @accept json

View File

@@ -13,7 +13,7 @@ import (
// @id HelmRepoSearch
// @summary Search Helm Charts
// @description
// @description **Access policy**: authorized
// @description **Access policy**: authenticated
// @tags helm
// @param repo query string true "Helm repository URL"
// @security jwt

View File

@@ -15,8 +15,8 @@ import (
// @id HelmShow
// @summary Show Helm Chart Information
// @description
// @description **Access policy**: authorized
// @tags helm_chart
// @description **Access policy**: authenticated
// @tags helm
// @param repo query string true "Helm repository URL"
// @param chart query string true "Chart name"
// @param command path string true "chart/values/readme"

View File

@@ -32,21 +32,23 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz
authorizationService: authorizationService,
}
kubeRouter := h.PathPrefix("/kubernetes/{id}").Subrouter()
kubeRouter := h.PathPrefix("/kubernetes").Subrouter()
kubeRouter.Use(bouncer.AuthenticatedAccess)
kubeRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
kubeRouter.Use(kubeOnlyMiddleware)
kubeRouter.PathPrefix("/config").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesConfig))).Methods(http.MethodGet)
kubeRouter.PathPrefix("/nodes_limits").Handler(
// endpoints
endpointRouter := kubeRouter.PathPrefix("/{id}").Subrouter()
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
endpointRouter.Use(kubeOnlyMiddleware)
endpointRouter.PathPrefix("/nodes_limits").Handler(
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.getKubernetesNodesLimits))).Methods(http.MethodGet)
// namespaces
// in the future this piece of code might be in another package (or a few different packages - namespaces/namespace?)
// to keep it simple, we've decided to leave it like this.
namespaceRouter := kubeRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
return h

View File

@@ -5,105 +5,203 @@ import (
"fmt"
"net/http"
clientV1 "k8s.io/client-go/tools/clientcmd/api/v1"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
bolterrors "github.com/portainer/portainer/api/bolt/errors"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
kcli "github.com/portainer/portainer/api/kubernetes/cli"
)
// @id GetKubernetesConfig
// @summary Generates kubeconfig file enabling client communication with k8s api server
// @description Generates kubeconfig file enabling client communication with k8s api server
// @description **Access policy**: authorized
// @description **Access policy**: authenticated
// @tags kubernetes
// @security jwt
// @accept json
// @produce json
// @param id path int true "Environment(Endpoint) identifier"
// @param ids query []int false "will include only these environments(endpoints)"
// @param excludeIds query []int false "will exclude these environments(endpoints)"
// @success 200 "Success"
// @failure 400 "Invalid request"
// @failure 401 "Unauthorized"
// @failure 403 "Permission denied"
// @failure 404 "Environment(Endpoint) or ServiceAccount not found"
// @failure 500 "Server error"
// @router /kubernetes/{id}/config [get]
// @router /kubernetes/config [get]
func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid environment identifier route variable", err}
}
endpoint, err := handler.dataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err == bolterrors.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an environment with the specified identifier inside the database", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an environment with the specified identifier inside the database", err}
}
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access environment", err}
}
bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err}
}
cli, err := handler.kubernetesClientFactory.GetKubeClient(endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
endpoints, handlerErr := handler.filterUserKubeEndpoints(r)
if handlerErr != nil {
return handlerErr
}
if len(endpoints) == 0 {
return &httperror.HandlerError{http.StatusBadRequest, "empty endpoints list", errors.New("empty endpoints list")}
}
apiServerURL := getProxyUrl(r, endpointID)
config, err := cli.GetKubeConfig(r.Context(), apiServerURL, bearerToken, tokenData)
if err != nil {
return &httperror.HandlerError{http.StatusNotFound, "Unable to generate Kubeconfig", err}
config, handlerErr := handler.buildConfig(r, tokenData, bearerToken, endpoints)
if handlerErr != nil {
return handlerErr
}
filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, endpoint.Name)
contentAcceptHeader := r.Header.Get("Accept")
if contentAcceptHeader == "text/yaml" {
return writeFileContent(w, r, endpoints, tokenData, config)
}
func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.Endpoint, *httperror.HandlerError) {
var endpointIDs []portainer.EndpointID
_ = request.RetrieveJSONQueryParameter(r, "ids", &endpointIDs, true)
var excludeEndpointIDs []portainer.EndpointID
_ = request.RetrieveJSONQueryParameter(r, "excludeIds", &excludeEndpointIDs, true)
if len(endpointIDs) > 0 && len(excludeEndpointIDs) > 0 {
return nil, &httperror.HandlerError{http.StatusBadRequest, "Can't provide both 'ids' and 'excludeIds' parameters", errors.New("invalid parameters")}
}
securityContext, err := security.RetrieveRestrictedRequestContext(r)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
}
endpointGroups, err := handler.dataStore.EndpointGroup().EndpointGroups()
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment groups from the database", err}
}
if len(endpointIDs) > 0 {
var endpoints []portainer.Endpoint
for _, endpointID := range endpointIDs {
endpoint, err := handler.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environment from the database", err}
}
if !endpointutils.IsKubernetesEndpoint(endpoint) {
continue
}
endpoints = append(endpoints, *endpoint)
}
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
return filteredEndpoints, nil
}
var kubeEndpoints []portainer.Endpoint
endpoints, err := handler.dataStore.Endpoint().Endpoints()
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve environments from the database", err}
}
for _, endpoint := range endpoints {
if !endpointutils.IsKubernetesEndpoint(&endpoint) {
continue
}
kubeEndpoints = append(kubeEndpoints, endpoint)
}
filteredEndpoints := security.FilterEndpoints(kubeEndpoints, endpointGroups, securityContext)
if len(excludeEndpointIDs) > 0 {
filteredEndpoints = endpointutils.FilterByExcludeIDs(filteredEndpoints, excludeEndpointIDs)
}
return filteredEndpoints, nil
}
func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenData, bearerToken string, endpoints []portainer.Endpoint) (*clientV1.Config, *httperror.HandlerError) {
configClusters := make([]clientV1.NamedCluster, len(endpoints))
configContexts := make([]clientV1.NamedContext, len(endpoints))
var configAuthInfos []clientV1.NamedAuthInfo
authInfosSet := make(map[string]bool)
for idx, endpoint := range endpoints {
cli, err := handler.kubernetesClientFactory.GetKubeClient(&endpoint)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err}
}
serviceAccount, err := cli.GetServiceAccount(tokenData)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, fmt.Sprintf("unable to find serviceaccount associated with user; username=%s", tokenData.Username), err}
}
configClusters[idx] = buildCluster(r, endpoint)
configContexts[idx] = buildContext(serviceAccount.Name, endpoint)
if !authInfosSet[serviceAccount.Name] {
configAuthInfos = append(configAuthInfos, buildAuthInfo(serviceAccount.Name, bearerToken))
authInfosSet[serviceAccount.Name] = true
}
}
return &clientV1.Config{
APIVersion: "v1",
Kind: "Config",
Clusters: configClusters,
Contexts: configContexts,
CurrentContext: configContexts[0].Name,
AuthInfos: configAuthInfos,
}, nil
}
func buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster {
proxyURL := fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpoint.ID)
return clientV1.NamedCluster{
Name: buildClusterName(endpoint.Name),
Cluster: clientV1.Cluster{
Server: proxyURL,
InsecureSkipTLSVerify: true,
},
}
}
func buildClusterName(endpointName string) string {
return fmt.Sprintf("portainer-cluster-%s", endpointName)
}
func buildContext(serviceAccountName string, endpoint portainer.Endpoint) clientV1.NamedContext {
contextName := fmt.Sprintf("portainer-ctx-%s", endpoint.Name)
return clientV1.NamedContext{
Name: contextName,
Context: clientV1.Context{
AuthInfo: serviceAccountName,
Cluster: buildClusterName(endpoint.Name),
},
}
}
func buildAuthInfo(serviceAccountName string, bearerToken string) clientV1.NamedAuthInfo {
return clientV1.NamedAuthInfo{
Name: serviceAccountName,
AuthInfo: clientV1.AuthInfo{
Token: bearerToken,
},
}
}
func writeFileContent(w http.ResponseWriter, r *http.Request, endpoints []portainer.Endpoint, tokenData *portainer.TokenData, config *clientV1.Config) *httperror.HandlerError {
filenameSuffix := "kubeconfig"
if len(endpoints) == 1 {
filenameSuffix = endpoints[0].Name
}
filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, filenameSuffix)
if r.Header.Get("Accept") == "text/yaml" {
yaml, err := kcli.GenerateYAML(config)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Failed to generate Kubeconfig", err}
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.yaml", filenameBase))
return YAML(w, yaml)
return response.YAML(w, yaml)
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.json", filenameBase))
return response.JSON(w, config)
}
// getProxyUrl generates portainer proxy url which acts as proxy to k8s api server
func getProxyUrl(r *http.Request, endpointID int) string {
return fmt.Sprintf("https://%s/api/endpoints/%d/kubernetes", r.Host, endpointID)
}
// YAML writes yaml response as string to writer. Returns a pointer to a HandlerError if encoding fails.
// This could be moved to a more useful place; but that place is most likely not in this project.
// It should actually go in https://github.com/portainer/libhttp - since that is from where we use response.JSON.
// We use `data interface{}` as parameter - since im trying to keep it as close to (or the same as) response.JSON method signature:
// https://github.com/portainer/libhttp/blob/d20481a3da823c619887c440a22fdf4fa8f318f2/response/response.go#L13
func YAML(rw http.ResponseWriter, data interface{}) *httperror.HandlerError {
rw.Header().Set("Content-Type", "text/yaml")
strData, ok := data.(string)
if !ok {
return &httperror.HandlerError{
StatusCode: http.StatusInternalServerError,
Message: "Unable to write YAML response",
Err: errors.New("failed to convert input to string"),
}
}
fmt.Fprint(rw, strData)
return nil
}

View File

@@ -13,7 +13,7 @@ import (
// @id getKubernetesNodesLimits
// @summary Get CPU and memory limits of all nodes within k8s cluster
// @description Get CPU and memory limits of all nodes within k8s cluster
// @description **Access policy**: authorized
// @description **Access policy**: authenticated
// @tags kubernetes
// @security jwt
// @accept json

View File

@@ -26,7 +26,9 @@ type motdData struct {
Style string `json:"style"`
}
// @id MOTD
// @summary fetches the message of the day
// @description **Access policy**: restricted
// @tags motd
// @security jwt
// @produce json

View File

@@ -81,7 +81,7 @@ func (payload *registryConfigurePayload) Validate(r *http.Request) error {
// @id RegistryConfigure
// @summary Configures a registry
// @description Configures a registry.
// @description **Access policy**: admin
// @description **Access policy**: restricted
// @tags registries
// @security jwt
// @accept json

View File

@@ -62,7 +62,7 @@ func (payload *registryCreatePayload) Validate(_ *http.Request) error {
// @id RegistryCreate
// @summary Create a new registry
// @description Create a new registry.
// @description **Access policy**: administrator
// @description **Access policy**: restricted
// @tags registries
// @security jwt
// @accept json

View File

@@ -15,7 +15,7 @@ import (
// @id RegistryDelete
// @summary Remove a registry
// @description Remove a registry
// @description **Access policy**: administrator
// @description **Access policy**: restricted
// @tags registries
// @security jwt
// @param id path int true "Registry identifier"

View File

@@ -14,7 +14,7 @@ import (
// @id RegistryInspect
// @summary Inspect a registry
// @description Retrieve details about a registry.
// @description **Access policy**: administrator
// @description **Access policy**: restricted
// @tags registries
// @security jwt
// @produce json

View File

@@ -37,7 +37,7 @@ func (payload *registryUpdatePayload) Validate(r *http.Request) error {
// @id RegistryUpdate
// @summary Update a registry
// @description Update a registry
// @description **Access policy**: administrator
// @description **Access policy**: restricted
// @tags registries
// @security jwt
// @accept json

View File

@@ -38,7 +38,7 @@ func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
// @id ResourceControlUpdate
// @summary Update a resource control
// @description Update a resource control
// @description **Access policy**: restricted
// @description **Access policy**: authenticated
// @tags resource_controls
// @security jwt
// @accept json

View File

@@ -12,6 +12,7 @@ import (
func hideFields(settings *portainer.Settings) {
settings.LDAPSettings.Password = ""
settings.OAuthSettings.ClientSecret = ""
settings.OAuthSettings.KubeSecretKey = nil
}
// Handler is the HTTP handler used to handle settings operations.

View File

@@ -16,6 +16,8 @@ type publicSettingsResponse struct {
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod" example:"1"`
// Whether edge compute features are enabled
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:"true"`
// Supported feature flags
Features map[portainer.Feature]bool `json:"Features"`
// The URL used for oauth login
OAuthLoginURI string `json:"OAuthLoginURI" example:"https://gitlab.com/oauth"`
// The URL used for oauth logout
@@ -52,6 +54,7 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures,
EnableTelemetry: appSettings.EnableTelemetry,
KubeconfigExpiry: appSettings.KubeconfigExpiry,
Features: appSettings.FeatureFlagSettings,
}
//if OAuth authentication is on, compose the related fields from application settings
if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth {

View File

@@ -121,6 +121,8 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
}
settings.HelmRepositoryURL = newHelmRepo
} else {
settings.HelmRepositoryURL = ""
}
if payload.BlackListedLabels != nil {
@@ -146,8 +148,13 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
if clientSecret == "" {
clientSecret = settings.OAuthSettings.ClientSecret
}
kubeSecret := payload.OAuthSettings.KubeSecretKey
if kubeSecret == nil {
kubeSecret = settings.OAuthSettings.KubeSecretKey
}
settings.OAuthSettings = *payload.OAuthSettings
settings.OAuthSettings.ClientSecret = clientSecret
settings.OAuthSettings.KubeSecretKey = kubeSecret
}
if payload.EnableEdgeComputeFeatures != nil {

View File

@@ -2,8 +2,8 @@ package stacks
import (
"fmt"
"log"
"net/http"
"path"
"strconv"
"time"
@@ -15,6 +15,7 @@ import (
"github.com/portainer/portainer/api/filesystem"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/stackutils"
)
type composeStackFromFileContentPayload struct {
@@ -36,6 +37,36 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err
}
return nil
}
func (handler *Handler) checkAndCleanStackDupFromSwarm(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID, stack *portainer.Stack) error {
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stackutils.ResourceControlID(stack.EndpointID, stack.Name), portainer.StackResourceControl)
if err != nil {
return err
}
// stop scheduler updates of the stack before removal
if stack.AutoUpdate != nil {
stopAutoupdate(stack.ID, stack.AutoUpdate.JobID, *handler.Scheduler)
}
err = handler.DataStore.Stack().DeleteStack(stack.ID)
if err != nil {
return err
}
if resourceControl != nil {
err = handler.DataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
if err != nil {
log.Printf("[ERROR] [Stack] Unable to remove the associated resource control from the database for stack: [%+v].", stack)
}
}
if exists, _ := handler.FileService.FileExists(stack.ProjectPath); exists {
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
if err != nil {
log.Printf("Unable to remove stack files from disk for stack: [%+v].", stack)
}
}
return nil
}
func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
var payload composeStackFromFileContentPayload
@@ -50,8 +81,22 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check for name collision", err}
}
if !isUnique {
return stackExistsError(payload.Name)
stacks, err := handler.DataStore.Stack().StacksByName(payload.Name)
if err != nil {
return stackExistsError(payload.Name)
}
for _, stack := range stacks {
if stack.Type != portainer.DockerComposeStack && stack.EndpointID == endpoint.ID {
err := handler.checkAndCleanStackDupFromSwarm(w, r, endpoint, userID, &stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
} else {
return stackExistsError(payload.Name)
}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@@ -155,8 +200,22 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return stackExistsError(payload.Name)
stacks, err := handler.DataStore.Stack().StacksByName(payload.Name)
if err != nil {
return stackExistsError(payload.Name)
}
for _, stack := range stacks {
if stack.Type != portainer.DockerComposeStack && stack.EndpointID == endpoint.ID {
err := handler.checkAndCleanStackDupFromSwarm(w, r, endpoint, userID, &stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
} else {
return stackExistsError(payload.Name)
}
}
}
//make sure the webhook ID is unique
@@ -284,8 +343,22 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to check for name collision", Err: err}
}
if !isUnique {
return stackExistsError(payload.Name)
stacks, err := handler.DataStore.Stack().StacksByName(payload.Name)
if err != nil {
return stackExistsError(payload.Name)
}
for _, stack := range stacks {
if stack.Type != portainer.DockerComposeStack && stack.EndpointID == endpoint.ID {
err := handler.checkAndCleanStackDupFromSwarm(w, r, endpoint, userID, &stack)
if err != nil {
return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err}
}
} else {
return stackExistsError(payload.Name)
}
}
}
stackID := handler.DataStore.Stack().GetNextIdentifier()
@@ -389,10 +462,9 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
!isAdminOrEndpointAdmin {
for _, file := range append([]string{config.stack.EntryPoint}, config.stack.AdditionalFiles...) {
path := path.Join(config.stack.ProjectPath, file)
stackContent, err := handler.FileService.GetFileContent(path)
stackContent, err := handler.FileService.GetFileContent(config.stack.ProjectPath, file)
if err != nil {
return errors.Wrapf(err, "failed to get stack file content `%q`", path)
return errors.Wrapf(err, "failed to get stack file content `%q`", file)
}
err = handler.isValidStackFile(stackContent, securitySettings)

View File

@@ -54,9 +54,6 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
if govalidator.IsNull(payload.StackFileContent) {
return errors.New("Invalid stack file content")
}
if govalidator.IsNull(payload.Namespace) {
return errors.New("Invalid namespace")
}
if govalidator.IsNull(payload.StackName) {
return errors.New("Invalid stack name")
}
@@ -64,9 +61,6 @@ func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) erro
}
func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Namespace) {
return errors.New("Invalid namespace")
}
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
return errors.New("Invalid repository URL. Must correspond to a valid URL format")
}

View File

@@ -3,7 +3,6 @@ package stacks
import (
"fmt"
"net/http"
"path"
"strconv"
"time"
@@ -399,8 +398,7 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
for _, file := range append([]string{config.stack.EntryPoint}, config.stack.AdditionalFiles...) {
path := path.Join(config.stack.ProjectPath, file)
stackContent, err := handler.FileService.GetFileContent(path)
stackContent, err := handler.FileService.GetFileContent(config.stack.ProjectPath, file)
if err != nil {
return errors.WithMessage(err, "failed to get stack file content")
}

View File

@@ -14,7 +14,22 @@ import (
"github.com/portainer/portainer/api/internal/stackutils"
)
// PUT request on /api/stacks/:id/associate?endpointId=<endpointId>&swarmId=<swarmId>&orphanedRunning=<orphanedRunning>
// @id StackAssociate
// @summary Associate an orphaned stack to a new environment(endpoint)
// @description **Access policy**: administrator
// @tags stacks
// @security jwt
// @produce json
// @param id path int true "Stack identifier"
// @param endpointId query int true "Stacks created before version 1.18.0 might not have an associated environment(endpoint) identifier. Use this optional parameter to set the environment(endpoint) identifier used by the stack."
// @param swarmId query int true "Swarm identifier"
// @param orphanedRunning query boolean true "Indicates whether the stack is orphaned"
// @success 200 {object} portainer.Stack "Success"
// @failure 400 "Invalid request"
// @failure 403 "Permission denied"
// @failure 404 "Stack not found"
// @failure 500 "Server error"
// @router /stacks/{id}/associate [put]
func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {

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