Compare commits

..

93 Commits

Author SHA1 Message Date
portainer-bot[bot] 245f905507 chore(release): version bump 2.33.8 (#2581)
Co-authored-by: Steven Kang <skan070@gmail.com>
2026-05-06 22:08:51 +00:00
Devon Steenberg 2029799c73 fix(libstack): pull images sequentially and respect COMPOSE_PARALLEL_LIMIT [BE-12930] (#2554) 2026-05-06 15:15:27 +12:00
andres-portainer d40462b756 fix(docker): add missing restrictions for Swarm BE-12772 (#2561) 2026-05-05 11:35:29 -03:00
andres-portainer d0018ce373 fix(docker): add more bind mount restriction checks BE-12771 (#2553) 2026-05-05 09:25:32 -03:00
Steven Kang b1d0f4708b chore(deps): remediate CVE-affected Go modules across agents, servers… - release 2.33.8 [R8S-1001] (#2543) 2026-05-05 12:47:58 +12:00
andres-portainer 8a46572174 fix(datastore): change EnforceEdgeID default to true BE-12925 (#2549) 2026-05-04 13:07:14 -03:00
andres-portainer 6ea3e22cc3 fix(docker): enforce bind mount restrictions for Mounts field BE-12770 (#2529) 2026-05-04 13:05:46 -03:00
andres-portainer a33cc4886a fix(git): forbid the usage of symlinks BE-12768 (#2533) 2026-05-04 12:57:08 -03:00
Steven Kang 7ae0affdca fix(chore): bump golang to 1.25.9 for remediating cves - release 2.33.8 [R8S-1000] (#2540) 2026-05-04 13:53:11 +12:00
Steven Kang 980485212d fix(security): upgrade Docker binary from v29.1.4 to v29.4.1 - release 2.33.8 [R8S-966] (#2537) 2026-05-04 13:51:39 +12:00
Oscar Zhou 26f5a94546 fix(security): backport k8s, archive, and boltdb fixes [BE-12928] (#2526)
Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
2026-05-01 11:27:11 -03:00
LP B b410694307 fix(api/docker): deny plugin related changes to regular users (#2298) 2026-04-20 17:08:28 +02:00
andres-portainer 27c04caa27 fix(websocket): remove the JWT token query string parameter BE-12833 (#2335) 2026-04-16 14:10:11 -03:00
LP B 97c8553e20 fix(api/custom_template): validate UAC when retrieving custom template file (#2300) 2026-04-14 14:24:12 +02:00
Oscar Zhou 379f4c3147 chore: bump version to 2.33.7 (#1832) 2026-02-10 09:48:08 +13:00
andres-portainer 21937dfe60 fix(security): fix CVE-2025-68121 by upgrading Go compiler BE-12581 (#1814) 2026-02-05 18:18:38 -03:00
Josiah Clumont 90946ceca5 fix(docker): Update the docker binary version that uses 1.25.6 to fix CVE-2025-61726 - for 2.33.7-LTS [R8S-818] (#1793) 2026-02-05 11:23:58 +13:00
Josiah Clumont 9cc3243166 fix(CVE): Update stdlib to 1.24.12 - for LTS 2.33 PATCH [R8S-816] (#1797) 2026-02-05 11:20:47 +13:00
Oscar Zhou 1f20add37f fix(edgestack): EntryFileName not found [BE-12499] (#1705) 2026-01-22 08:44:04 +13:00
LP B 60733427e6 fix(app/edge): UI form error on edge stack update (#1644) 2026-01-13 17:15:59 +01:00
LP B 3f451830cb fix(app): generate a container name when names list is empty (#1616) 2026-01-07 19:52:41 +01:00
Chaim Lev-Ari 9f0facc0f3 chore(build): migrate to pnpm (#1577) 2025-12-30 11:42:33 +02:00
andres-portainer a622122486 fix(edgegroups): fix a nil pointer dereference BE-12487 (#1574) 2025-12-29 15:06:06 -03:00
andres-portainer 12fdc45ee5 fix(compose): upgrade compose-go to v2.40.3 to fix a nil panic BE-12424 (#1552) 2025-12-23 18:12:05 -03:00
Viktor Pettersson abf3d1450d fix(docs): ensure all docs related dependencies, such as struct types are available before building swagger docs PLA-542 (#1563) 2025-12-22 15:03:21 +13:00
Yajith Dayarathna 1ae795d508 chore: ci workflow(round3) (#1549) 2025-12-22 10:55:00 +13:00
Devon Steenberg 6f9ddd47de fix(swarm): stack deployments [BE-12478] (#1547)
This commit https://github.com/docker/cli/commit/9b9d103b297cdff32e35dde771c8c392c7caabeb, introduced in docker 29, changed the behaviour of how the --tlsXXX flags are handled. Before this change leading and trailing quotes would be stripped. This meant that an invalid path that we were passing for the tls ca cert was being cleaned up to be an empty string. To preserve the old behaviour we now pass an empty string.
2025-12-17 13:23:34 +13:00
Steven Kang 9507cf9d8b chore: version bump 2.33.6 (#1541) 2025-12-16 08:40:59 +09:00
Chaim Lev-Ari 76e4054215 fix(containers): clear mac address on edit/duplicate [BE-12436] (#1537) 2025-12-15 09:59:53 +02:00
Oscar Zhou 0a3e13915c fix(stack): stack start failed with private image [BE-12464] (#1529) 2025-12-12 11:00:28 +13:00
Steven Kang dbd6e49e5f fix(security): cve-2025-47914 and 58181 - release 2.33.6 [R8S-714] (#1519) 2025-12-11 15:22:28 +09:00
Chaim Lev-Ari abad58a370 fix(docker/services): ignore missing EndpointSpec (#1509) 2025-12-10 10:28:38 +02:00
Oscar Zhou 4eb1c7b11f fix(stack/remote): fail to pull image in stack with relative path enabled [BE-12237] (#1499) 2025-12-10 08:31:08 +13:00
LP B 3afedce570 fix(api): do not give away information on error (#1497) 2025-12-08 16:50:10 -03:00
LP B a7b6db72a5 fix(compose): use project in compose start options (#1498) 2025-12-08 19:46:31 +01:00
Yajith Dayarathna 9c79d6dc7d chore(ci): minor ci workflow updates (#1492) 2025-12-08 14:12:43 +13:00
Steven Kang 11f612a501 chore: version bump 2.33.5 (#1448) 2025-11-27 08:03:27 +09:00
Oscar Zhou cb8d8fcfd6 fix(snapshot): prevent from returning SnapshotRaw data [BE-12431] (#1443) 2025-11-26 12:56:55 +13:00
Devon Steenberg 22bb1e604d fix(docker): bump docker max api version [BE-12399] (#1405) 2025-11-21 15:28:17 +13:00
Steven Kang 970b135261 chore: version bump 2.33.4 (#1419) 2025-11-20 10:16:25 +13:00
Steven Kang a69470ec08 fix: CVE-2024-25621 - release 2.33.4 [R8S-639] (#1413) 2025-11-18 17:34:12 +13:00
Steven Kang ea6f1c97f5 fix: CVE-2025-47913 - release 2.33.4 [R8S-638] (#1402) 2025-11-18 16:28:11 +13:00
Steven Kang 6d058987f3 fix: CVE-2025-47906 and CVE-2025-47910 - release 2.33.4 [R8S-618] (#1400) 2025-11-18 08:57:06 +13:00
Oscar Zhou 6998f05855 fix(edgestack): selected registry does not show in edge stack edit page [BE-12384] (#1394) 2025-11-17 17:30:37 +13:00
Chaim Lev-Ari 94d01c58fc fix(widget): remove fixed margin on button [BE-12344] (#1369) 2025-11-09 16:50:49 +02:00
Steven Kang d98eb77067 chore: version bump 2.33.3 (#1351) 2025-10-30 11:47:33 +13:00
andres-portainer 941e86563a fix(CVE-2025-62725): upgrade github.com/docker/compose/v2 to v2.40.2 BE-12352 (#1344) 2025-10-29 18:17:39 -03:00
Malcolm Lockyer f72d6b97d3 fix(agent): for iamra and ecr login, detect errors and retry [be-12284] (#1309) 2025-10-29 17:24:02 +13:00
Steven Kang 32926aa8bf fix: add web socket headers for kubeconfig based access - 2.33.3 [r8s-592] (#1329) 2025-10-22 09:44:46 +13:00
Steven Kang 1849c61c38 fix: display dependency version for kubectl and helm - 2.33.3 [R8S-501] (#1282) 2025-10-07 16:23:43 +13:00
andres-portainer fd6d74602c feat(boltdb): attempt to compact using a read-only database BE-12287 (#1268) 2025-09-30 19:10:16 -03:00
Oscar Zhou 74b1dd04d1 fix(k8s): memory leak during k8s stack deployment [BE-12281] (#1264) 2025-09-30 18:00:12 +13:00
Steven Kang 7450501b7a chore: version bump 2.33.2 (#1257) 2025-09-25 14:29:28 +12:00
andres-portainer dcfe2d9809 feat(database): add a flag to compact on startup BE-12283 (#1256) 2025-09-24 18:43:54 -03:00
Ali c21c91632f fix(rbac): redirect on unauthorized namespace [r8s-564] (#1246)
Merging because this PR doesn't introduce any CI failures, compared to the release 2.33 CI run https://github.com/portainer/portainer-suite/actions/runs/17957775674
2025-09-24 13:22:42 +12:00
andres-portainer 732337615e fix(edgestacks): add a missing webhook uniqueness check BE-12219 (#1251) 2025-09-23 17:20:25 -03:00
LP B 6ea16c0060 fix(api/endpoints): edge stack status type filter no longer always include Pending envs (#1230) 2025-09-22 16:10:46 +02:00
Ali 4e7d4b60a5 fix(cve): fix frontend CVEs [r8s-563] (#1238) 2025-09-22 10:17:12 +12:00
Oscar Zhou 19e1cc2fbd fix(activitylog): remove export limit and fix search function [BE-12270] (#1232) 2025-09-19 14:45:14 +12:00
andres-portainer 68b9fef3f0 fix(kubernetes/cli): fix a data-race BE-12259 (#1227) 2025-09-18 10:22:29 -03:00
Viktor Pettersson 1e47df6611 chore(go): upgrade Go to 1.24.6 BE-12263 (#1220) 2025-09-18 11:44:09 +12:00
Oscar Zhou 405ce8f671 feat(edge): add option to allow always clone git repository [BE-12240] (#1207) 2025-09-17 18:25:47 +12:00
andres-portainer e9d31b3b7b fix(csp): update the Content-Security-Policy header BE-12228 (#1202) 2025-09-15 10:47:57 -03:00
LP B f97adc94ad fix(api/custom-templates): UAC-allowed users cannot fetch custom template details (#1199) 2025-09-12 15:38:58 +02:00
Malcolm Lockyer 11d6341765 fix(encryption): set correct default secret key path release [r8s-555] (#1184)
Co-authored-by: Gorbasch <57012534+mbegerau@users.noreply.github.com>
2025-09-11 16:32:52 +12:00
andres-portainer c3cf46b0e0 fix(auth): remove a nil pointer dereference BE-12149 (#1174) 2025-09-10 21:55:19 -03:00
andres-portainer ff746beba1 fix(csp): add google.com to the CSP header BE-12228 (#1176) 2025-09-10 15:01:00 -03:00
LP B da1672fc17 fix(api): standard users cannot connect or disconnect containers to networks (#1166) 2025-09-09 22:07:24 +02:00
Ali 7a9376cbaf fix(helm): update helm repo validation to match helm cli [r8s-531] (#1142) 2025-09-08 08:55:57 +12:00
Malcolm Lockyer c0f6410d80 fix(fips): encrypt the chisel private key file for fips [be-12132] (#1149) 2025-09-05 13:17:23 +12:00
andres-portainer 4b9ab98fd2 fix(git): add a minimum interval validation BE-12220 (#1145) 2025-09-04 15:11:24 -03:00
andres-portainer 3354ee4e4b fix(registries): clear sensitive fields in the update handler BE-12215 (#1129) 2025-09-03 10:41:27 -03:00
Steven Kang af3c45bea0 chore: version bump 2.33.1 (#1108) 2025-08-27 10:45:29 +12:00
andres-portainer 816a6f9bef chore(bbolt): upgrade bbolt to v1.4.3 BE-12193 (#1104) 2025-08-25 17:59:33 -03:00
Devon Steenberg e86ea22900 fix(sslflags): Deprecate ssl flags [BE-12168] (#1076) 2025-08-25 20:25:07 +12:00
Malcolm Lockyer 12b2acbc00 fix(standard): manual endpoint refresh fails to save new status [be-12188] (#1096) 2025-08-25 13:49:04 +12:00
Ali 4a8b42928e fix(environments): create k8s specific edge agent before connecting [r8s-438] (#1086)
Merging because this change is unrelated to the failing kubernetes/tests/helm-oci.spec.ts tests
2025-08-25 09:32:16 +12:00
Oscar Zhou 2e828b39da fix(autoupdate): update tooltips in edge stack gitops update [BE-12177] (#1080) 2025-08-23 10:55:57 +12:00
Steven Kang 49c6521c23 fix: GHSA-2464-8j7c-4cjm - release 2.33 [R8S-495] (#1089) 2025-08-22 14:03:16 +12:00
Steven Kang debf1a742b chore: version bump 2.33.0 (#1065) 2025-08-20 11:28:05 +12:00
James Player 5d3708ec3e fix(UI): add experimental features back in [r8s-483] (#1060) 2025-08-19 17:07:27 +12:00
Steven Kang 9320fd4c50 fix: cve-2025-55198 and cve-2025-55199 - release 2.33 [R8S-482] (#1058) 2025-08-19 16:22:54 +12:00
Steven Kang 974682bd98 chore: version bump to 2.33.0-rc2 (#1054) 2025-08-19 11:04:56 +12:00
Ali 631f1deb2e fix(helm): support http and custom tls helm registries, give help when misconfigured [r8s-472] (#1032)
Co-authored-by: JamesPlayer <james.player@portainer.io>
2025-08-18 12:07:41 +12:00
LP B 4169b045fb fix(api/edge-stacks): avoid overriding updates with old values (#1048) 2025-08-16 03:52:21 +02:00
andres-portainer 0a2a786aa3 fix(migrator): rewrite a migration so it is idempotent BE-12053 (#1043) 2025-08-15 09:18:31 -03:00
James Player 808f87206e fix(ui): Fixed react-select TooManyResultsSelector filter and improved scrolling (#1028) 2025-08-15 15:33:43 +12:00
Cara Ryan ed6fa82904 fix(pending-actions): Small improvements to pending actions (R8S-350) (#1025) 2025-08-15 10:51:45 +12:00
andres-portainer 9fc301110b fix(crypto): replace fips140 calls with fips calls BE-11979 (#1035) 2025-08-14 19:36:05 -03:00
andres-portainer 69101ac89a feat(openai): remove OpenAI BE-12018 (#1034) 2025-08-14 19:35:43 -03:00
Malcolm Lockyer 69d33dd432 fix(fips): use standard lib pbkdf2 [be-12164] (#1037) 2025-08-15 09:45:49 +12:00
Ali 389cbf748c fix(logs): improve log rendering performance [r8s-437] (#1023)
Merging because the same tests are failing in CE develop https://github.com/portainer/system-tests/actions/runs/16953578581
2025-08-14 13:53:35 +12:00
LP B d01b31f707 feat(api): Permissions-Policy header deny all (#1022) 2025-08-13 22:07:52 +02:00
2262 changed files with 29620 additions and 92780 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules/
dist/
test/
+157
View File
@@ -0,0 +1,157 @@
env:
browser: true
jquery: true
node: true
es6: true
globals:
angular: true
extends:
- 'eslint:recommended'
- 'plugin:storybook/recommended'
- 'plugin:import/typescript'
- prettier
plugins:
- import
parserOptions:
ecmaVersion: latest
sourceType: module
project: './tsconfig.json'
ecmaFeatures:
modules: true
rules:
no-console: error
no-alert: error
no-control-regex: 'off'
no-empty: warn
no-empty-function: warn
no-useless-escape: 'off'
import/named: error
import/order:
[
'error',
{
pathGroups:
[
{ pattern: '@@/**', group: 'internal', position: 'after' },
{ pattern: '@/**', group: 'internal' },
{ pattern: '{Kubernetes,Portainer,Agent,Azure,Docker}/**', group: 'internal' },
],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
pathGroupsExcludedImportTypes: ['internal'],
},
]
no-restricted-imports:
- error
- patterns:
- group:
- '@/react/test-utils/*'
message: 'These utils are just for test files'
settings:
'import/resolver':
alias:
map:
- ['@@', './app/react/components']
- ['@', './app']
extensions: ['.js', '.ts', '.tsx']
typescript: true
node: true
overrides:
- files:
- app/**/*.ts{,x}
parserOptions:
project: './tsconfig.json'
parser: '@typescript-eslint/parser'
plugins:
- '@typescript-eslint'
- 'regex'
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'
- 'plugin:storybook/recommended'
- prettier # should be last
settings:
react:
version: 'detect'
rules:
no-console: error
import/order:
[
'error',
{
pathGroups: [{ pattern: '@@/**', group: 'internal', position: 'after' }, { pattern: '@/**', group: 'internal' }],
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
},
]
no-plusplus: off
func-style: [error, 'declaration']
import/prefer-default-export: off
no-use-before-define: 'off'
'@typescript-eslint/no-use-before-define': ['error', { functions: false, 'allowNamedExports': true }]
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
no-underscore-dangle: 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
controlComponents:
- Input
- Checkbox
'jsx-a11y/control-has-associated-label': off
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
'react/jsx-no-bind': off
'no-await-in-loop': 'off'
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }]
'regex/invalid': ['error', [{ 'regex': '<Icon icon="(.*)"', 'message': 'Please directly import the `lucide-react` icon instead of using the string' }]]
'@typescript-eslint/no-restricted-imports':
- error
- patterns:
- group:
- '@/react/test-utils/*'
message: 'These utils are just for test files'
overrides: # allow props spreading for hoc files
- files:
- app/**/with*.ts{,x}
rules:
'react/jsx-props-no-spreading': off
- files:
- app/**/*.test.*
extends:
- 'plugin:vitest/recommended'
env:
'vitest/env': true
rules:
'react/jsx-no-constructed-context-values': off
'@typescript-eslint/no-restricted-imports': off
no-restricted-imports: off
'react/jsx-props-no-spreading': off
- files:
- app/**/*.stories.*
rules:
'no-alert': off
'@typescript-eslint/no-restricted-imports': off
no-restricted-imports: off
'react/jsx-props-no-spreading': off
+1 -1
View File
@@ -6,7 +6,7 @@ body:
Thanks for suggesting an idea for Portainer!
Before opening a new idea or feature request, make sure that we do not have any duplicates already open. You can ensure this by [searching this discussion category](https://github.com/orgs/portainer/discussions/categories/ideas). If there is a duplicate, please add a comment to the existing idea instead.
Before opening a new idea or feature request, make sure that we do not have any duplicates already open. You can ensure this by [searching this discussion cagetory](https://github.com/orgs/portainer/discussions/categories/ideas). If there is a duplicate, please add a comment to the existing idea instead.
Also, be sure to check our [knowledge base](https://portal.portainer.io/knowledge) and [documentation](https://docs.portainer.io) as they may point you toward a solution.
+5 -18
View File
@@ -22,7 +22,7 @@ body:
options:
- label: Yes, I've searched similar issues on [GitHub](https://github.com/portainer/portainer/issues).
required: true
- label: Yes, I've checked whether this issue is covered in the Portainer [documentation](https://docs.portainer.io).
- label: Yes, I've checked whether this issue is covered in the Portainer [documentation](https://docs.portainer.io) or [knowledge base](https://portal.portainer.io/knowledge).
required: true
- type: markdown
@@ -94,23 +94,6 @@ body:
description: We only provide support for current versions of Portainer as per the lifecycle policy linked above. If you are on an older version of Portainer we recommend [updating first](https://docs.portainer.io/start/upgrade) in case your bug has already been fixed.
multiple: false
options:
- '2.40.0'
- '2.39.1'
- '2.39.0'
- '2.38.1'
- '2.38.0'
- '2.37.0'
- '2.36.0'
- '2.35.0'
- '2.34.0'
- '2.33.7'
- '2.33.6'
- '2.33.5'
- '2.33.4'
- '2.33.3'
- '2.33.2'
- '2.33.1'
- '2.33.0'
- '2.32.0'
- '2.31.3'
- '2.31.2'
@@ -143,6 +126,10 @@ body:
- '2.22.0'
- '2.21.5'
- '2.21.4'
- '2.21.3'
- '2.21.2'
- '2.21.1'
- '2.21.0'
validations:
required: true
-2
View File
@@ -18,5 +18,3 @@ api/docs
.env
go.work.sum
.vitest
-13
View File
@@ -1,13 +0,0 @@
version: "2"
linters:
default: none
enable:
- forbidigo
settings:
forbidigo:
forbid:
- pattern: ^dataservices.DataStore.(EdgeGroup|EdgeJob|EdgeStack|EndpointRelation|Endpoint|GitCredential|Registry|ResourceControl|Role|Settings|Snapshot|SSLSettings|Stack|Tag|User)$
msg: Use a transaction instead
- pattern: ^(filepath|path)\.Join$
msg: Use filesystem.JoinPaths() from github.com/portainer/portainer/api/filesystem to prevent path traversal attacks
analyze-types: true
+1 -50
View File
@@ -1,14 +1,10 @@
version: "2"
run:
allow-parallel-runners: true
linters:
default: none
enable:
- bodyclose
- copyloopvar
- depguard
- errcheck
- errorlint
- forbidigo
- govet
@@ -17,18 +13,6 @@ linters:
- perfsprint
- staticcheck
- unused
- mirror
- durationcheck
- errorlint
- govet
- usetesting
- zerologlint
- testifylint
- modernize
- unconvert
- unused
- zerologlint
- exptostd
settings:
staticcheck:
checks: ["all", "-ST1003", "-ST1005", "-ST1016", "-SA1019", "-QF1003"]
@@ -48,44 +32,12 @@ linters:
desc: use github.com/portainer/portainer/pkg/libcrypto
- pkg: github.com/portainer/libhttp
desc: use github.com/portainer/portainer/pkg/libhttp
- pkg: golang.org/x/crypto
desc: golang.org/x/crypto is not allowed because of FIPS mode
- pkg: github.com/ProtonMail/go-crypto/openpgp
desc: github.com/ProtonMail/go-crypto/openpgp is not allowed because of FIPS mode
- pkg: github.com/cosi-project/runtime
desc: github.com/cosi-project/runtime is not allowed because of FIPS mode
- pkg: gopkg.in/yaml.v2
desc: use go.yaml.in/yaml/v3 instead
- pkg: gopkg.in/yaml.v3
desc: use go.yaml.in/yaml/v3 instead
- pkg: github.com/golang-jwt/jwt/v4
desc: use github.com/golang-jwt/jwt/v5 instead
- pkg: github.com/mitchellh/mapstructure
desc: use github.com/go-viper/mapstructure/v2 instead
- pkg: gopkg.in/alecthomas/kingpin.v2
desc: use github.com/alecthomas/kingpin/v2 instead
- pkg: github.com/jcmturner/gokrb5$
desc: use github.com/jcmturner/gokrb5/v8 instead
- pkg: github.com/gofrs/uuid
desc: use github.com/google/uuid
- pkg: github.com/Masterminds/semver$
desc: use github.com/Masterminds/semver/v3
- pkg: github.com/blang/semver
desc: use github.com/Masterminds/semver/v3
- pkg: github.com/coreos/go-semver
desc: use github.com/Masterminds/semver/v3
- pkg: github.com/hashicorp/go-version
desc: use github.com/Masterminds/semver/v3
forbidigo:
forbid:
- pattern: ^tls\.Config$
msg: Use crypto.CreateTLSConfiguration() instead
- pattern: ^tls\.Config\.(InsecureSkipVerify|MinVersion|MaxVersion|CipherSuites|CurvePreferences)$
msg: Do not set this field directly, use crypto.CreateTLSConfiguration() instead
- pattern: ^object\.(Commit|Tag)\.Verify$
msg: "Not allowed because of FIPS mode"
- pattern: ^(types\.SystemContext\.)?(DockerDaemonInsecureSkipTLSVerify|DockerInsecureSkipTLSVerify|OCIInsecureSkipTLSVerify)$
msg: "Not allowed because of FIPS mode"
analyze-types: true
exclusions:
generated: lax
@@ -93,13 +45,12 @@ linters:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
exclusions:
generated: lax
paths:
+1 -2
View File
@@ -1,3 +1,2 @@
dist
api/datastore/test_data
coverage
api/datastore/test_data
+9 -6
View File
@@ -5,18 +5,21 @@
"trailingComma": "es5",
"overrides": [
{
"files": ["*.html"],
"files": [
"*.html"
],
"options": {
"parser": "angular"
}
},
{
"files": ["*.{j,t}sx", "*.ts"],
"files": [
"*.{j,t}sx",
"*.ts"
],
"options": {
"printWidth": 80
}
}
],
"plugins": ["prettier-plugin-tailwindcss"],
"tailwindFunctions": ["clsx"]
}
]
}
+18 -38
View File
@@ -1,7 +1,6 @@
import path from 'path';
import { StorybookConfig } from '@storybook/react-webpack5';
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import { Configuration } from 'webpack';
import postcss from 'postcss';
@@ -10,38 +9,20 @@ const config: StorybookConfig = {
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-webpack5-compiler-swc',
'@chromatic-com/storybook',
{
name: '@storybook/addon-styling-webpack',
name: '@storybook/addon-styling',
options: {
rules: [
{
test: /\.css$/,
sideEffects: true,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
modules: {
localIdentName: '[path][name]__[local]',
auto: true,
exportLocalsConvention: 'camelCaseOnly',
},
},
},
{
loader: require.resolve('postcss-loader'),
options: {
implementation: postcss,
},
},
],
cssLoaderOptions: {
importLoaders: 1,
modules: {
localIdentName: '[path][name]__[local]',
auto: true,
exportLocalsConvention: 'camelCaseOnly',
},
],
},
postCss: {
implementation: postcss,
},
},
},
],
@@ -86,7 +67,12 @@ const config: StorybookConfig = {
...config,
resolve: {
...config.resolve,
tsconfig: path.resolve(__dirname, '..', 'tsconfig.json'),
plugins: [
...(config.resolve?.plugins || []),
new TsconfigPathsPlugin({
extensions: config.resolve?.extensions,
}),
],
},
module: {
...config.module,
@@ -97,17 +83,11 @@ const config: StorybookConfig = {
staticDirs: ['./public'],
typescript: {
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
compilerOptions: {
outDir: path.resolve(__dirname, '..', 'dist/public'),
},
},
},
framework: {
name: '@storybook/react-webpack5',
options: {},
},
docs: {},
};
export default config;
+18 -22
View File
@@ -1,9 +1,9 @@
import '../app/assets/css';
import React from 'react';
import { pushStateLocationPlugin, UIRouter } from '@uirouter/react';
import { initialize as initMSW, mswLoader } from 'msw-storybook-addon';
import { handlers } from '../app/setup-tests/server-handlers';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Preview } from '@storybook/react';
initMSW(
{
@@ -21,35 +21,31 @@ initMSW(
handlers
);
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers,
},
};
const testQueryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
const preview: Preview = {
decorators: (Story) => (
export const decorators = [
(Story) => (
<QueryClientProvider client={testQueryClient}>
<UIRouter plugins={[pushStateLocationPlugin]}>
<Story />
</UIRouter>
</QueryClientProvider>
),
loaders: [mswLoader],
parameters: {
options: {
storySort: {
order: ['Design System', 'Components', '*'],
},
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers,
},
},
};
];
export default preview;
export const loaders = [mswLoader];
+75 -129
View File
@@ -1,29 +1,27 @@
/* eslint-disable */
/* tslint:disable */
import { v4 as uuidv4 } from 'uuid';
/**
* Mock Service Worker.
* Mock Service Worker (2.0.11).
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
* - Please do NOT serve this file on production.
*/
const PACKAGE_VERSION = '2.12.10';
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82';
const INTEGRITY_CHECKSUM = 'c5f7f8e188b673ea4e677df7ea3c5a39';
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse');
const activeClientIds = new Set();
addEventListener('install', function () {
self.addEventListener('install', function () {
self.skipWaiting();
});
addEventListener('activate', function (event) {
self.addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim());
});
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id');
self.addEventListener('message', async function (event) {
const clientId = event.source.id;
if (!clientId || !self.clients) {
return;
@@ -50,10 +48,7 @@ addEventListener('message', async function (event) {
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
payload: INTEGRITY_CHECKSUM,
});
break;
}
@@ -63,16 +58,16 @@ addEventListener('message', async function (event) {
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
payload: true,
});
break;
}
case 'MOCK_DEACTIVATE': {
activeClientIds.delete(clientId);
break;
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId);
@@ -90,91 +85,72 @@ addEventListener('message', async function (event) {
}
});
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now();
self.addEventListener('fetch', function (event) {
const { request } = event;
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
if (request.mode === 'navigate') {
return;
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') {
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
return;
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
// after it's been deleted (still remains active until the next reload).
if (activeClientIds.size === 0) {
return;
}
const requestId = uuidv4();
event.respondWith(handleRequest(event, requestId, requestInterceptedAt));
// Generate unique request ID.
const requestId = crypto.randomUUID();
event.respondWith(handleRequest(event, requestId));
});
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
async function handleRequest(event, requestId) {
const client = await resolveMainClient(event);
const requestCloneForEvents = event.request.clone();
const response = await getResponse(event, client, requestId, requestInterceptedAt);
const response = await getResponse(event, client, requestId);
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents);
(async function () {
const responseClone = response.clone();
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone();
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
requestId,
isMockedResponse: IS_MOCKED_RESPONSE in response,
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
headers: Object.fromEntries(responseClone.headers.entries()),
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : []
);
[responseClone.body]
);
})();
}
return response;
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
// Resolve the main client for the given event.
// Client that issues a request doesn't necessarily equal the client
// that registered the worker. It's with the latter the worker should
// communicate with during the response resolving phase.
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId);
if (activeClientIds.has(event.clientId)) {
return client;
}
if (client?.frameType === 'top-level') {
return client;
}
@@ -195,37 +171,20 @@ async function resolveMainClient(event) {
});
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
async function getResponse(event, client, requestId) {
const { request } = event;
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone();
const requestClone = request.clone();
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers);
const headers = Object.fromEntries(requestClone.headers.entries());
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept');
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim());
const filteredValues = values.filter((value) => value !== 'msw/passthrough');
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '));
} else {
headers.delete('accept');
}
}
// Remove internal MSW request header so the passthrough request
// complies with any potential CORS preflight checks on the server.
// Some servers forbid unknown request headers.
delete headers['x-msw-intention'];
return fetch(requestClone, { headers });
}
@@ -243,19 +202,37 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
return passthrough();
}
// Bypass requests with the explicit bypass header.
// Such requests can be issued by "ctx.fetch()".
const mswIntention = request.headers.get('x-msw-intention');
if (['bypass', 'passthrough'].includes(mswIntention)) {
return passthrough();
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request);
const requestBuffer = await request.arrayBuffer();
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest,
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: requestBuffer,
keepalive: request.keepalive,
},
},
[serializedRequest.body]
[requestBuffer]
);
switch (clientMessage.type) {
@@ -263,7 +240,7 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
return respondWithMock(clientMessage.data);
}
case 'PASSTHROUGH': {
case 'MOCK_NOT_FOUND': {
return passthrough();
}
}
@@ -271,12 +248,6 @@ async function getResponse(event, client, requestId, requestInterceptedAt) {
return passthrough();
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
@@ -289,15 +260,11 @@ function sendToClient(client, message, transferrables = []) {
resolve(event.data);
};
client.postMessage(message, [channel.port2, ...transferrables.filter(Boolean)]);
client.postMessage(message, [channel.port2].concat(transferrables.filter(Boolean)));
});
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
async function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
@@ -315,24 +282,3 @@ function respondWithMock(response) {
return mockedResponse;
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
};
}
-59
View File
@@ -1,59 +0,0 @@
# Portainer Community Edition
Open-source container management platform with full Docker and Kubernetes support.
## Project Structure
For a detailed breakdown of frontend and backend directory layout, feature locations, and common development tasks, see [docs/guidelines/project-structure.md](../../docs/guidelines/project-structure.md).
## Frontend Guidelines
- [docs/guidelines/frontend-conventions.md](../../docs/guidelines/frontend-conventions.md) — component structure, React Query patterns, shared components, forms, theming
- [docs/guidelines/typescript-conventions.md](../../docs/guidelines/typescript-conventions.md) — types, anti-patterns, union types, named constants
- [docs/guidelines/frontend-unit-testing.md](../../docs/guidelines/frontend-unit-testing.md) — Vitest, React Testing Library
## Backend Guidelines
- [docs/guidelines/go-conventions.md](../../docs/guidelines/go-conventions.md) — error handling, naming, testing, code style
- [docs/guidelines/server-architecture.md](../../docs/guidelines/server-architecture.md) — Clean Architecture layers, transactions, CE/EE sharing patterns
- [docs/guidelines/logging.md](../../docs/guidelines/logging.md) — zerolog usage, log levels, message style
- [docs/guidelines/backend-code-reusability.md](../../docs/guidelines/backend-code-reusability.md) — how CE and EE share backend code
## Package Manager
- **PNPM** 10+ (for frontend)
- **Go** 1.26.1 (for backend)
## Build Commands
```bash
# Full build
make build # Build both client and server
make build-client # Build React/AngularJS frontend
make build-server # Build Go binary
make build-image # Build Docker image
# Development
make dev # Run both in dev mode
make dev-client # Start webpack-dev-server (port 8999)
make dev-server # Run containerized Go server
# Frontend
pnpm dev # Webpack dev server
pnpm build # Build frontend with webpack
pnpm typecheck # Run typecheck for frontend (with tsc)
pnpm lint # lint frontend (with eslint)
pnpm test # test frontend (with vitest)
pnpm format # format frontend (with prettier)
# Testing
make test # All tests (backend + frontend)
make test-server # Backend tests only
make lint # Lint all code
make format # Format code
```
## Development Servers
- Frontend: http://localhost:8999
- Backend: http://localhost:9000 (HTTP) / https://localhost:9443 (HTTPS)
+17 -14
View File
@@ -1,11 +1,16 @@
# See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63
# For a list of valid GOOS and GOARCH values
# Note: these can be overriden on the command line e.g. `make PLATFORM=<platform> ARCH=<arch>`
PLATFORM=$(shell go env GOOS)
ARCH=$(shell go env GOARCH)
# build target, can be one of "production", "testing", "development"
ENV=development
WEBPACK_CONFIG=webpack/webpack.$(ENV).js
TAG=local
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
GOTESTSUM_VERSION?=v1.13.0
GOTESTSUM=go run gotest.tools/gotestsum@$(GOTESTSUM_VERSION)
GOTESTSUM=go run gotest.tools/gotestsum@latest
# Don't change anything below this line unless you know what you're doing
.DEFAULT_GOAL := help
@@ -32,6 +37,10 @@ build-image: build-all ## Build the Portainer image locally
build-storybook: ## Build and serve the storybook files
pnpm run storybook:build
devops: clean deps build-client ## Build the everything target specifically for CI
echo "Building the devops binary..."
@./build/build_binary_azuredevops.sh "$(PLATFORM)" "$(ARCH)"
##@ Build dependencies
.PHONY: deps server-deps client-deps tidy
deps: server-deps client-deps ## Download all client and server build dependancies
@@ -45,12 +54,14 @@ client-deps: ## Install client dependencies
tidy: ## Tidy up the go.mod file
@go mod tidy
##@ Cleanup
.PHONY: clean
clean: ## Remove all build and download artifacts
@echo "Clearing the dist directory..."
@rm -rf dist/*
##@ Testing
.PHONY: test test-client test-server
test: test-server test-client ## Run all tests
@@ -58,10 +69,8 @@ test: test-server test-client ## Run all tests
test-client: ## Run client tests
pnpm run test $(ARGS) --coverage
TEST_PACKAGES?=./...
test-server: ## Run server tests
$(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover -covermode=atomic -coverprofile=coverage.out $(TEST_PACKAGES)
$(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover -covermode=atomic -coverprofile=coverage.out ./...
##@ Dev
.PHONY: dev dev-client dev-server
@@ -70,7 +79,7 @@ dev: ## Run both the client and server in development mode
make dev-client
dev-client: ## Run the client in development mode
pnpm install && pnpm run dev
pnpm run dev
dev-server: build-server ## Run the server in development mode
@./dev/run_container.sh
@@ -98,13 +107,14 @@ lint-client: ## Lint client code
lint-server: tidy ## Lint server code
golangci-lint run --timeout=10m -c .golangci.yaml
golangci-lint run --timeout=10m --new-from-rev=HEAD~ -c .golangci-forward.yaml
##@ Extension
.PHONY: dev-extension
dev-extension: build-server build-client ## Run the extension in development mode
make local -f build/docker-extension/Makefile
##@ Docs
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
docs-build: init-dist ## Build docs
@@ -115,13 +125,6 @@ docs-validate: docs-build ## Validate docs
pnpm swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
pnpm swagger-cli validate dist/docs/openapi.yaml
.PHONY: docs-serve
docs-serve: docs-build ## Serve docs locally with Swagger UI on port 8080
docker run -p 8080:8080 \
-e SWAGGER_JSON=/foo/swagger.yaml \
-v $(PWD)/dist/docs:/foo \
swaggerapi/swagger-ui
##@ Helpers
.PHONY: help
help: ## Display this help
+1 -1
View File
@@ -46,7 +46,7 @@ You can join the Portainer Community by visiting [https://www.portainer.io/join-
## Security
For information about reporting security vulnerabilities, please see our [Security Policy](SECURITY.md).
- 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
-60
View File
@@ -1,60 +0,0 @@
# Security Policy
## Supported Versions
Portainer maintains both Short-Term Support (STS) and Long-Term Support (LTS) versions in accordance with our official [Portainer Lifecycle Policy](https://docs.portainer.io/start/lifecycle).
| Version Type | Support Status |
| ------------------------ | ------------------------------------------- |
| LTS (Long-Term Support) | Supported for critical security fixes |
| STS (Short-Term Support) | Supported until the next STS or LTS release |
| Legacy / EOL | Not supported |
For a detailed breakdown of current versions and their specific End of Life (EOL) dates,
please refer to the [Portainer Lifecycle Policy](https://docs.portainer.io/start/lifecycle).
## Reporting a Vulnerability
The Portainer team takes the security of our products seriously. If you believe you have found a security vulnerability in any Portainer-owned repository, please report it to us responsibly.
**Please do not report security vulnerabilities via public GitHub issues.**
### Disclosure Process
1. **Report**: You can report in one of two ways:
- **GitHub**: Use the **Report a vulnerability** button on the **Security** tab of this repository.
- **Email**: Send your findings to security@portainer.io.
2. **Details**: To help us verify the issue, please include:
- A description of the vulnerability and its potential impact.
- Step-by-step instructions to reproduce the issue (e.g. proof-of-concept code, scripts, or screenshots).
- The version of the software and the environment in which it was found.
3. **Acknowledge**: We will acknowledge receipt of your report and provide an initial assessment.
4. **Resolution**: We will work to resolve the issue as quickly as possible. We request that you do not disclose the vulnerability publicly until we have released a fix and notified affected users.
## Our Commitment
If you follow the responsible disclosure process, we will:
- Respond to your report in a timely manner.
- Provide an estimated timeline for remediation.
- Notify you when the vulnerability has been patched.
- Give credit for the discovery (if desired) once the fix is public.
We will make every effort to promptly address any security weaknesses. Security advisories and fixes will be published through GitHub Security Advisories and other channels as needed.
Thank you for helping keep Portainer and our community secure.
## Resources
- [Contributing to Portainer](https://docs.portainer.io/contribute/contribute#contributing-to-the-portainer-ce-codebase)
+8 -4
View File
@@ -19,22 +19,24 @@ const RedirectReasonAdminInitTimeout string = "AdminInitTimeout"
type Monitor struct {
timeout time.Duration
datastore dataservices.DataStore
shutdownCtx context.Context
cancellationFunc context.CancelFunc
mu sync.RWMutex
adminInitDisabled bool
}
// New creates a monitor that when started will wait for the timeout duration and then shutdown the application unless it has been initialized.
func New(timeout time.Duration, datastore dataservices.DataStore) *Monitor {
func New(timeout time.Duration, datastore dataservices.DataStore, shutdownCtx context.Context) *Monitor {
return &Monitor{
timeout: timeout,
datastore: datastore,
shutdownCtx: shutdownCtx,
adminInitDisabled: false,
}
}
// Start starts the monitor. The monitor will stop when ctx is cancelled, or when Stop is called.
func (m *Monitor) Start(ctx context.Context) {
// Starts starts the monitor. Active monitor could be stopped or shuttted down by cancelling the shutdown context.
func (m *Monitor) Start() {
m.mu.Lock()
defer m.mu.Unlock()
@@ -42,7 +44,7 @@ func (m *Monitor) Start(ctx context.Context) {
return
}
cancellationCtx, cancellationFunc := context.WithCancel(ctx)
cancellationCtx, cancellationFunc := context.WithCancel(context.Background())
m.cancellationFunc = cancellationFunc
go func() {
@@ -67,6 +69,8 @@ func (m *Monitor) Start(ctx context.Context) {
}
case <-cancellationCtx.Done():
log.Debug().Msg("canceling initialization monitor")
case <-m.shutdownCtx.Done():
log.Debug().Msg("shutting down initialization monitor")
}
}()
}
+10 -19
View File
@@ -1,8 +1,8 @@
package adminmonitor
import (
"context"
"testing"
"testing/synctest"
"time"
portainer "github.com/portainer/portainer/api"
@@ -11,28 +11,21 @@ import (
)
func Test_stopWithoutStarting(t *testing.T) {
t.Parallel()
monitor := New(1*time.Minute, nil)
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
}
func Test_stopCouldBeCalledMultipleTimes(t *testing.T) {
t.Parallel()
monitor := New(1*time.Minute, nil)
monitor := New(1*time.Minute, nil, nil)
monitor.Stop()
monitor.Stop()
}
func Test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) {
t.Parallel()
synctest.Test(t, test_startOrStopCouldBeCalledMultipleTimesConcurrently)
}
monitor := New(1*time.Minute, nil, context.Background())
func test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) {
monitor := New(1*time.Minute, nil)
go monitor.Start(t.Context())
monitor.Start(t.Context())
go monitor.Start()
monitor.Start()
go monitor.Stop()
monitor.Stop()
@@ -41,9 +34,8 @@ func test_startOrStopCouldBeCalledMultipleTimesConcurrently(t *testing.T) {
}
func Test_canStopStartedMonitor(t *testing.T) {
t.Parallel()
monitor := New(1*time.Minute, nil)
monitor.Start(t.Context())
monitor := New(1*time.Minute, nil, context.Background())
monitor.Start()
assert.NotNil(t, monitor.cancellationFunc, "cancellation function is missing in started monitor")
monitor.Stop()
@@ -51,12 +43,11 @@ func Test_canStopStartedMonitor(t *testing.T) {
}
func Test_start_shouldDisableInstanceAfterTimeout_ifNotInitialized(t *testing.T) {
t.Parallel()
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
monitor := New(timeout, datastore)
monitor.Start(t.Context())
monitor := New(timeout, datastore, context.Background())
monitor.Start()
<-time.After(20 * timeout)
assert.True(t, monitor.WasInstanceDisabled(), "monitor should have been timeout and instance is disabled")
+8 -8
View File
@@ -11,18 +11,20 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/url"
"github.com/rs/zerolog/log"
)
// GetAgentVersionAndPlatform returns the agent version and platform
//
// it sends a ping to the agent and parses the version and platform from the headers
func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (portainer.AgentPlatform, string, error) { //nolint:forbidigo
httpCli := &http.Client{Timeout: 3 * time.Second}
httpCli := &http.Client{
Timeout: 3 * time.Second,
}
if tlsConfig != nil {
httpCli.Transport = &http.Transport{TLSClientConfig: tlsConfig}
httpCli.Transport = &http.Transport{
TLSClientConfig: tlsConfig,
}
}
parsedURL, err := url.ParseURL(endpointUrl + "/ping")
@@ -42,10 +44,8 @@ func GetAgentVersionAndPlatform(endpointUrl string, tlsConfig *tls.Config) (port
return 0, "", err
}
_, _ = io.Copy(io.Discard, resp.Body)
if err := resp.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return 0, "", fmt.Errorf("Failed request with status %d", resp.StatusCode)
+12 -13
View File
@@ -7,35 +7,34 @@ import (
)
func Test_generateRandomKey(t *testing.T) {
t.Parallel()
is := assert.New(t)
tests := []struct {
name string
wantLength int
name string
wantLenth int
}{
{
name: "Generate a random key of length 16",
wantLength: 16,
name: "Generate a random key of length 16",
wantLenth: 16,
},
{
name: "Generate a random key of length 32",
wantLength: 32,
name: "Generate a random key of length 32",
wantLenth: 32,
},
{
name: "Generate a random key of length 64",
wantLength: 64,
name: "Generate a random key of length 64",
wantLenth: 64,
},
{
name: "Generate a random key of length 128",
wantLength: 128,
name: "Generate a random key of length 128",
wantLenth: 128,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GenerateRandomKey(tt.wantLength)
is.Len(got, tt.wantLength)
got := GenerateRandomKey(tt.wantLenth)
is.Equal(tt.wantLenth, len(got))
})
}
+1 -1
View File
@@ -71,7 +71,7 @@ func (c *ApiKeyCache[T]) InvalidateUserKeyCache(userId portainer.UserID) bool {
for _, k := range c.cache.Keys() {
user, _, _ := c.Get(k.(string))
if c.userCmpFn(user, userId) {
present = c.cache.Remove(k) || present
present = c.cache.Remove(k)
}
}
-5
View File
@@ -8,7 +8,6 @@ import (
)
func Test_apiKeyCacheGet(t *testing.T) {
t.Parallel()
is := assert.New(t)
keyCache := NewAPIKeyCache(10, compareUser)
@@ -44,7 +43,6 @@ func Test_apiKeyCacheGet(t *testing.T) {
}
func Test_apiKeyCacheSet(t *testing.T) {
t.Parallel()
is := assert.New(t)
keyCache := NewAPIKeyCache(10, compareUser)
@@ -70,7 +68,6 @@ func Test_apiKeyCacheSet(t *testing.T) {
}
func Test_apiKeyCacheDelete(t *testing.T) {
t.Parallel()
is := assert.New(t)
keyCache := NewAPIKeyCache(10, compareUser)
@@ -90,7 +87,6 @@ func Test_apiKeyCacheDelete(t *testing.T) {
}
func Test_apiKeyCacheLRU(t *testing.T) {
t.Parallel()
is := assert.New(t)
tests := []struct {
@@ -152,7 +148,6 @@ func Test_apiKeyCacheLRU(t *testing.T) {
}
func Test_apiKeyCacheInvalidateUserKeyCache(t *testing.T) {
t.Parallel()
is := assert.New(t)
keyCache := NewAPIKeyCache(10, compareUser)
+31 -43
View File
@@ -10,20 +10,17 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_SatisfiesAPIKeyServiceInterface(t *testing.T) {
t.Parallel()
is := assert.New(t)
is.Implements((*APIKeyService)(nil), NewAPIKeyService(nil, nil))
}
func Test_GenerateApiKey(t *testing.T) {
t.Parallel()
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
@@ -33,7 +30,7 @@ func Test_GenerateApiKey(t *testing.T) {
t.Run("Successfully generates API key", func(t *testing.T) {
desc := "test-1"
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, desc)
require.NoError(t, err)
is.NoError(err)
is.NotEmpty(rawKey)
is.NotEmpty(apiKey)
is.Equal(desc, apiKey.Description)
@@ -41,7 +38,7 @@ func Test_GenerateApiKey(t *testing.T) {
t.Run("Api key prefix is 7 chars", func(t *testing.T) {
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-2")
require.NoError(t, err)
is.NoError(err)
is.Equal(rawKey[:7], apiKey.Prefix)
is.Len(apiKey.Prefix, 7)
@@ -49,7 +46,7 @@ func Test_GenerateApiKey(t *testing.T) {
t.Run("Api key has 'ptr_' as prefix", func(t *testing.T) {
rawKey, _, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x")
require.NoError(t, err)
is.NoError(err)
is.Equal(portainerAPIKeyPrefix, "ptr_")
is.True(strings.HasPrefix(rawKey, "ptr_"))
@@ -58,7 +55,7 @@ func Test_GenerateApiKey(t *testing.T) {
t.Run("Successfully caches API key", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-3")
require.NoError(t, err)
is.NoError(err)
userFromCache, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
@@ -68,7 +65,7 @@ func Test_GenerateApiKey(t *testing.T) {
t.Run("Decoded raw api-key digest matches generated digest", func(t *testing.T) {
rawKey, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-4")
require.NoError(t, err)
is.NoError(err)
generatedDigest := sha256.Sum256([]byte(rawKey))
@@ -77,7 +74,6 @@ func Test_GenerateApiKey(t *testing.T) {
}
func Test_GetAPIKey(t *testing.T) {
t.Parallel()
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
@@ -87,17 +83,16 @@ func Test_GetAPIKey(t *testing.T) {
t.Run("Successfully returns all API keys", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
require.NoError(t, err)
is.NoError(err)
apiKeyGot, err := service.GetAPIKey(apiKey.ID)
require.NoError(t, err)
is.NoError(err)
is.Equal(apiKey, apiKeyGot)
})
}
func Test_GetAPIKeys(t *testing.T) {
t.Parallel()
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
@@ -107,18 +102,17 @@ func Test_GetAPIKeys(t *testing.T) {
t.Run("Successfully returns all API keys", func(t *testing.T) {
user := portainer.User{ID: 1}
_, _, err := service.GenerateApiKey(user, "test-1")
require.NoError(t, err)
is.NoError(err)
_, _, err = service.GenerateApiKey(user, "test-2")
require.NoError(t, err)
is.NoError(err)
keys, err := service.GetAPIKeys(user.ID)
require.NoError(t, err)
is.NoError(err)
is.Len(keys, 2)
})
}
func Test_GetDigestUserAndKey(t *testing.T) {
t.Parallel()
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
@@ -128,10 +122,10 @@ func Test_GetDigestUserAndKey(t *testing.T) {
t.Run("Successfully returns user and api key associated to digest", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
require.NoError(t, err)
is.NoError(err)
userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
require.NoError(t, err)
is.NoError(err)
is.Equal(user, userGot)
is.Equal(*apiKey, apiKeyGot)
})
@@ -139,10 +133,10 @@ func Test_GetDigestUserAndKey(t *testing.T) {
t.Run("Successfully caches user and api key associated to digest", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
require.NoError(t, err)
is.NoError(err)
userGot, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
require.NoError(t, err)
is.NoError(err)
is.Equal(user, userGot)
is.Equal(*apiKey, apiKeyGot)
@@ -154,7 +148,6 @@ func Test_GetDigestUserAndKey(t *testing.T) {
}
func Test_UpdateAPIKey(t *testing.T) {
t.Parallel()
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
@@ -163,19 +156,16 @@ func Test_UpdateAPIKey(t *testing.T) {
t.Run("Successfully updates the api-key LastUsed time", func(t *testing.T) {
user := portainer.User{ID: 1}
err := store.User().Create(&user)
require.NoError(t, err)
store.User().Create(&user)
_, apiKey, err := service.GenerateApiKey(user, "test-x")
require.NoError(t, err)
is.NoError(err)
apiKey.LastUsed = time.Now().UTC().Unix()
err = service.UpdateAPIKey(apiKey)
require.NoError(t, err)
is.NoError(err)
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
require.NoError(t, err)
is.NoError(err)
log.Debug().Str("wanted", fmt.Sprintf("%+v", apiKey)).Str("got", fmt.Sprintf("%+v", apiKeyGot)).Msg("")
@@ -184,7 +174,7 @@ func Test_UpdateAPIKey(t *testing.T) {
t.Run("Successfully updates api-key in cache upon api-key update", func(t *testing.T) {
_, apiKey, err := service.GenerateApiKey(portainer.User{ID: 1}, "test-x2")
require.NoError(t, err)
is.NoError(err)
_, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
@@ -194,7 +184,7 @@ func Test_UpdateAPIKey(t *testing.T) {
is.NotEqual(*apiKey, apiKeyFromCache)
err = service.UpdateAPIKey(apiKey)
require.NoError(t, err)
is.NoError(err)
_, updatedAPIKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
@@ -203,7 +193,6 @@ func Test_UpdateAPIKey(t *testing.T) {
}
func Test_DeleteAPIKey(t *testing.T) {
t.Parallel()
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
@@ -213,30 +202,30 @@ func Test_DeleteAPIKey(t *testing.T) {
t.Run("Successfully updates the api-key", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
require.NoError(t, err)
is.NoError(err)
_, apiKeyGot, err := service.GetDigestUserAndKey(apiKey.Digest)
require.NoError(t, err)
is.NoError(err)
is.Equal(*apiKey, apiKeyGot)
err = service.DeleteAPIKey(apiKey.ID)
require.NoError(t, err)
is.NoError(err)
_, _, err = service.GetDigestUserAndKey(apiKey.Digest)
require.Error(t, err)
is.Error(err)
})
t.Run("Successfully removes api-key from cache upon deletion", func(t *testing.T) {
user := portainer.User{ID: 1}
_, apiKey, err := service.GenerateApiKey(user, "test-1")
require.NoError(t, err)
is.NoError(err)
_, apiKeyFromCache, ok := service.cache.Get(apiKey.Digest)
is.True(ok)
is.Equal(*apiKey, apiKeyFromCache)
err = service.DeleteAPIKey(apiKey.ID)
require.NoError(t, err)
is.NoError(err)
_, _, ok = service.cache.Get(apiKey.Digest)
is.False(ok)
@@ -244,7 +233,6 @@ func Test_DeleteAPIKey(t *testing.T) {
}
func Test_InvalidateUserKeyCache(t *testing.T) {
t.Parallel()
is := assert.New(t)
_, store := datastore.MustNewTestStore(t, true, true)
@@ -255,10 +243,10 @@ func Test_InvalidateUserKeyCache(t *testing.T) {
// generate api keys
user := portainer.User{ID: 1}
_, apiKey1, err := service.GenerateApiKey(user, "test-1")
require.NoError(t, err)
is.NoError(err)
_, apiKey2, err := service.GenerateApiKey(user, "test-2")
require.NoError(t, err)
is.NoError(err)
// verify api keys are present in cache
_, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest)
@@ -285,11 +273,11 @@ func Test_InvalidateUserKeyCache(t *testing.T) {
// generate keys for 2 users
user1 := portainer.User{ID: 1}
_, apiKey1, err := service.GenerateApiKey(user1, "test-1")
require.NoError(t, err)
is.NoError(err)
user2 := portainer.User{ID: 2}
_, apiKey2, err := service.GenerateApiKey(user2, "test-2")
require.NoError(t, err)
is.NoError(err)
// verify keys in cache
_, apiKeyFromCache, ok := service.cache.Get(apiKey1.Digest)
+14 -6
View File
@@ -17,15 +17,18 @@ func TarFileInBuffer(fileContent []byte, fileName string, mode int64) ([]byte, e
Size: int64(len(fileContent)),
}
if err := tarWriter.WriteHeader(header); err != nil {
err := tarWriter.WriteHeader(header)
if err != nil {
return nil, err
}
if _, err := tarWriter.Write(fileContent); err != nil {
_, err = tarWriter.Write(fileContent)
if err != nil {
return nil, err
}
if err := tarWriter.Close(); err != nil {
err = tarWriter.Close()
if err != nil {
return nil, err
}
@@ -40,7 +43,10 @@ type tarFileInBuffer struct {
func NewTarFileInBuffer() *tarFileInBuffer {
var b bytes.Buffer
return &tarFileInBuffer{b: &b, w: tar.NewWriter(&b)}
return &tarFileInBuffer{
b: &b,
w: tar.NewWriter(&b),
}
}
// Put puts a single file to tar archive buffer.
@@ -55,9 +61,11 @@ func (t *tarFileInBuffer) Put(fileContent []byte, fileName string, mode int64) e
return err
}
_, err := t.w.Write(fileContent)
if _, err := t.w.Write(fileContent); err != nil {
return err
}
return err
return nil
}
// Bytes returns the archive as a byte array.
+5 -7
View File
@@ -11,7 +11,6 @@ import (
"strings"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/logs"
)
// TarGzDir creates a tar.gz archive and returns it's path.
@@ -23,13 +22,12 @@ func TarGzDir(absolutePath string) (string, error) {
if err != nil {
return "", err
}
defer logs.CloseAndLogErr(outFile)
defer outFile.Close()
zipWriter := gzip.NewWriter(outFile)
defer logs.CloseAndLogErr(zipWriter)
defer zipWriter.Close()
tarWriter := tar.NewWriter(zipWriter)
defer logs.CloseAndLogErr(tarWriter)
defer tarWriter.Close()
err = filepath.Walk(absolutePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
@@ -90,7 +88,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
if err != nil {
return err
}
defer logs.CloseAndLogErr(zipReader)
defer zipReader.Close()
tarReader := tar.NewReader(zipReader)
@@ -120,7 +118,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
if _, err := io.Copy(outFile, tarReader); err != nil {
return fmt.Errorf("Failed to extract file %s", header.Name)
}
logs.CloseAndLogErr(outFile)
outFile.Close()
default:
return fmt.Errorf("tar: unknown type: %v in %s",
header.Typeflag,
+18 -43
View File
@@ -5,53 +5,39 @@ import (
"compress/gzip"
"os"
"os/exec"
"path"
"path/filepath"
"testing"
"github.com/portainer/portainer/api/filesystem"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func listFiles(dir string) []string {
items := make([]string, 0)
if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if path == dir {
return nil
}
items = append(items, path)
return nil
}); err != nil {
log.Warn().Err(err).Msg("failed to list files in directory")
}
})
return items
}
func Test_shouldCreateArchive(t *testing.T) {
t.Parallel()
tmpdir := t.TempDir()
content := []byte("content")
err := os.WriteFile(filesystem.JoinPaths(tmpdir, "outer"), content, 0600)
require.NoError(t, err)
err = os.MkdirAll(filesystem.JoinPaths(tmpdir, "dir"), 0700)
require.NoError(t, err)
err = os.WriteFile(filesystem.JoinPaths(tmpdir, "dir", ".dotfile"), content, 0600)
require.NoError(t, err)
err = os.WriteFile(filesystem.JoinPaths(tmpdir, "dir", "inner"), content, 0600)
require.NoError(t, err)
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
require.NoError(t, err)
assert.Equal(t, filesystem.JoinPaths(tmpdir, filepath.Base(tmpdir)+".tar.gz"), gzPath)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, filepath.Base(tmpdir)+".tar.gz"), gzPath)
extractionDir := t.TempDir()
cmd := exec.Command("tar", "-xzf", gzPath, "-C", extractionDir)
@@ -61,10 +47,9 @@ func Test_shouldCreateArchive(t *testing.T) {
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := filesystem.JoinPaths(extractionDir, p)
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, err := os.ReadFile(fullpath)
require.NoError(t, err)
copyContent, _ := os.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
}
@@ -74,25 +59,16 @@ func Test_shouldCreateArchive(t *testing.T) {
}
func Test_shouldCreateArchive2(t *testing.T) {
t.Parallel()
tmpdir := t.TempDir()
content := []byte("content")
err := os.WriteFile(filesystem.JoinPaths(tmpdir, "outer"), content, 0600)
require.NoError(t, err)
err = os.MkdirAll(filesystem.JoinPaths(tmpdir, "dir"), 0700)
require.NoError(t, err)
err = os.WriteFile(filesystem.JoinPaths(tmpdir, "dir", ".dotfile"), content, 0600)
require.NoError(t, err)
err = os.WriteFile(filesystem.JoinPaths(tmpdir, "dir", "inner"), content, 0600)
require.NoError(t, err)
os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
os.WriteFile(path.Join(tmpdir, "dir", "inner"), content, 0600)
gzPath, err := TarGzDir(tmpdir)
require.NoError(t, err)
assert.Equal(t, filesystem.JoinPaths(tmpdir, filepath.Base(tmpdir)+".tar.gz"), gzPath)
assert.Nil(t, err)
assert.Equal(t, filepath.Join(tmpdir, filepath.Base(tmpdir)+".tar.gz"), gzPath)
extractionDir := t.TempDir()
r, _ := os.Open(gzPath)
@@ -102,7 +78,7 @@ func Test_shouldCreateArchive2(t *testing.T) {
extractedFiles := listFiles(extractionDir)
wasExtracted := func(p string) {
fullpath := filesystem.JoinPaths(extractionDir, p)
fullpath := path.Join(extractionDir, p)
assert.Contains(t, extractedFiles, fullpath)
copyContent, _ := os.ReadFile(fullpath)
assert.Equal(t, content, copyContent)
@@ -114,7 +90,6 @@ func Test_shouldCreateArchive2(t *testing.T) {
}
func TestExtractTarGzPathTraversal(t *testing.T) {
t.Parallel()
testDir := t.TempDir()
// Create an evil file with a path traversal attempt
+4 -8
View File
@@ -8,8 +8,6 @@ import (
"path/filepath"
"strings"
"github.com/portainer/portainer/api/logs"
"github.com/pkg/errors"
)
@@ -20,7 +18,7 @@ func UnzipFile(src string, dest string) error {
if err != nil {
return err
}
defer logs.CloseAndLogErr(r)
defer r.Close()
for _, f := range r.File {
p := filepath.Join(dest, f.Name)
@@ -32,9 +30,7 @@ func UnzipFile(src string, dest string) error {
if f.FileInfo().IsDir() {
// Make Folder
if err := os.MkdirAll(p, os.ModePerm); err != nil {
return err
}
os.MkdirAll(p, os.ModePerm)
continue
}
@@ -57,13 +53,13 @@ func unzipFile(f *zip.File, p string) error {
if err != nil {
return errors.Wrapf(err, "unzipFile: can't create file %s", p)
}
defer logs.CloseAndLogErr(outFile)
defer outFile.Close()
rc, err := f.Open()
if err != nil {
return errors.Wrapf(err, "unzipFile: can't open zip file %s in the archive", f.Name)
}
defer logs.CloseAndLogErr(rc)
defer rc.Close()
if _, err = io.Copy(outFile, rc); err != nil {
return errors.Wrapf(err, "unzipFile: can't copy an archived file content")
+5 -8
View File
@@ -1,16 +1,13 @@
package archive
import (
"path/filepath"
"testing"
"github.com/portainer/portainer/api/filesystem"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUnzipFile(t *testing.T) {
t.Parallel()
dir := t.TempDir()
/*
Archive structure.
@@ -23,10 +20,10 @@ func TestUnzipFile(t *testing.T) {
err := UnzipFile("./testdata/sample_archive.zip", dir)
require.NoError(t, err)
assert.NoError(t, err)
archiveDir := dir + "/sample_archive"
assert.FileExists(t, filesystem.JoinPaths(archiveDir, "0.txt"))
assert.FileExists(t, filesystem.JoinPaths(archiveDir, "0", "1.txt"))
assert.FileExists(t, filesystem.JoinPaths(archiveDir, "0", "1", "2.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1.txt"))
assert.FileExists(t, filepath.Join(archiveDir, "0", "1", "2.txt"))
}
-9
View File
@@ -6,15 +6,6 @@ import (
"github.com/aws/aws-sdk-go-v2/service/ecr"
)
// Registry represents an ECR registry endpoint information.
// This struct is used to parse and validate ECR endpoint URLs.
type Registry struct {
ID string // AWS account ID (empty for accountless endpoints like "ecr-fips.us-west-1.amazonaws.com")
FIPS bool // Whether this is a FIPS endpoint (contains "-fips" in the URL)
Region string // AWS region (e.g., "us-east-1", "us-gov-west-1")
Public bool // Whether this is ecr-public.aws.com
}
type (
Service struct {
accessKey string
-70
View File
@@ -1,70 +0,0 @@
package ecr
import (
"fmt"
"net/url"
"regexp"
"strings"
)
// ecrEndpointPattern matches all valid ECR endpoints including account-prefixed and accountless formats.
// Based on AWS ECR credential helper regex but extended to support accountless endpoints.
//
// Supported formats:
// - Account-prefixed: 123456789012.dkr.ecr-fips.us-east-1.amazonaws.com
// - Account-prefixed (hyphen): 123456789012.dkr-ecr-fips.us-west-1.on.aws
// - Accountless service: ecr-fips.us-west-1.amazonaws.com
// - Accountless API: ecr-fips.us-east-1.api.aws
// - Non-FIPS variants: All formats above without "-fips"
//
// Regex groups:
// - Group 1: Full account prefix (optional) - e.g., "123456789012.dkr." or "123456789012.dkr-"
// - Group 2: Account ID (optional) - e.g., "123456789012"
// - Group 3: FIPS flag (optional) - either "-fips" or empty string
// - Group 4: Region - e.g., "us-east-1", "us-gov-west-1"
// - Group 5: Domain suffix - e.g., "amazonaws.com", "api.aws"
var ecrEndpointPattern = regexp.MustCompile(
`^((\d{12})\.dkr[\.\-])?ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.(amazonaws\.(?:com(?:\.cn)?|eu)|api\.aws|on\.(?:aws|amazonwebservices\.com\.cn)|sc2s\.sgov\.gov|c2s\.ic\.gov|cloud\.adc-e\.uk|csp\.hci\.ic\.gov)$`,
)
// ParseECREndpoint parses an ECR registry URL and extracts registry information.
// This function replaces the AWS ECR credential helper library's ExtractRegistry function,
// which only supports account-prefixed endpoints.
//
// Reference: https://docs.aws.amazon.com/general/latest/gr/ecr.html
func ParseECREndpoint(urlStr string) (*Registry, error) {
// Normalize URL by adding https:// prefix if not present
if !strings.HasPrefix(urlStr, "https://") && !strings.HasPrefix(urlStr, "http://") {
urlStr = "https://" + urlStr
}
u, err := url.Parse(urlStr)
if err != nil {
return nil, fmt.Errorf("invalid URL: %w", err)
}
hostname := u.Hostname()
// Special case: ECR Public
// ECR Public uses a different domain and doesn't have FIPS variant
if hostname == "ecr-public.aws.com" {
return &Registry{
FIPS: false,
Public: true,
}, nil
}
// Parse standard ECR endpoints using regex
matches := ecrEndpointPattern.FindStringSubmatch(hostname)
if len(matches) == 0 {
return nil, fmt.Errorf("not a valid ECR endpoint: %s", hostname)
}
return &Registry{
ID: matches[2], // Account ID (may be empty for accountless endpoints)
FIPS: matches[3] == "-fips", // Check if "-fips" is present
Region: matches[4], // AWS region
Public: false,
}, nil
}
-254
View File
@@ -1,254 +0,0 @@
package ecr
import (
"testing"
)
func TestParseECREndpoint(t *testing.T) {
t.Parallel()
tests := []struct {
name string
url string
want *Registry
wantError bool
}{
// Standard AWS Commercial - Account-prefixed FIPS
{
name: "account-prefixed FIPS us-east-1",
url: "123456789012.dkr.ecr-fips.us-east-1.amazonaws.com",
want: &Registry{
ID: "123456789012",
FIPS: true,
Region: "us-east-1",
Public: false,
},
},
{
name: "account-prefixed FIPS us-west-2",
url: "123456789012.dkr.ecr-fips.us-west-2.amazonaws.com",
want: &Registry{
ID: "123456789012",
FIPS: true,
Region: "us-west-2",
Public: false,
},
},
// Accountless FIPS service endpoints
{
name: "accountless FIPS us-west-1",
url: "ecr-fips.us-west-1.amazonaws.com",
want: &Registry{
ID: "",
FIPS: true,
Region: "us-west-1",
Public: false,
},
},
{
name: "accountless FIPS us-east-2",
url: "ecr-fips.us-east-2.amazonaws.com",
want: &Registry{
ID: "",
FIPS: true,
Region: "us-east-2",
Public: false,
},
},
// Accountless FIPS API endpoints
{
name: "accountless FIPS API us-west-1",
url: "ecr-fips.us-west-1.api.aws",
want: &Registry{
ID: "",
FIPS: true,
Region: "us-west-1",
Public: false,
},
},
{
name: "accountless FIPS API us-east-1",
url: "ecr-fips.us-east-1.api.aws",
want: &Registry{
ID: "",
FIPS: true,
Region: "us-east-1",
Public: false,
},
},
// on.aws domain with hyphen separator
{
name: "account-prefixed FIPS hyphen us-west-1",
url: "123456789012.dkr-ecr-fips.us-west-1.on.aws",
want: &Registry{
ID: "123456789012",
FIPS: true,
Region: "us-west-1",
Public: false,
},
},
{
name: "account-prefixed FIPS hyphen us-east-2",
url: "123456789012.dkr-ecr-fips.us-east-2.on.aws",
want: &Registry{
ID: "123456789012",
FIPS: true,
Region: "us-east-2",
Public: false,
},
},
// AWS GovCloud
{
name: "account-prefixed FIPS us-gov-east-1",
url: "123456789012.dkr.ecr-fips.us-gov-east-1.amazonaws.com",
want: &Registry{
ID: "123456789012",
FIPS: true,
Region: "us-gov-east-1",
Public: false,
},
},
{
name: "account-prefixed FIPS us-gov-west-1",
url: "123456789012.dkr.ecr-fips.us-gov-west-1.amazonaws.com",
want: &Registry{
ID: "123456789012",
FIPS: true,
Region: "us-gov-west-1",
Public: false,
},
},
{
name: "accountless FIPS us-gov-west-1",
url: "ecr-fips.us-gov-west-1.amazonaws.com",
want: &Registry{
ID: "",
FIPS: true,
Region: "us-gov-west-1",
Public: false,
},
},
{
name: "accountless FIPS API us-gov-east-1",
url: "ecr-fips.us-gov-east-1.api.aws",
want: &Registry{
ID: "",
FIPS: true,
Region: "us-gov-east-1",
Public: false,
},
},
// ECR Public
{
name: "ecr-public",
url: "ecr-public.aws.com",
want: &Registry{
ID: "",
FIPS: false,
Region: "",
Public: true,
},
},
// Non-FIPS endpoints (valid ECR but FIPS=false)
{
name: "account-prefixed non-FIPS us-east-1",
url: "123456789012.dkr.ecr.us-east-1.amazonaws.com",
want: &Registry{
ID: "123456789012",
FIPS: false,
Region: "us-east-1",
Public: false,
},
},
{
name: "accountless non-FIPS us-west-1",
url: "ecr.us-west-1.amazonaws.com",
want: &Registry{
ID: "",
FIPS: false,
Region: "us-west-1",
Public: false,
},
},
{
name: "accountless non-FIPS API us-east-2",
url: "ecr.us-east-2.api.aws",
want: &Registry{
ID: "",
FIPS: false,
Region: "us-east-2",
Public: false,
},
},
// URLs with https:// prefix
{
name: "with https prefix",
url: "https://ecr-fips.us-west-1.amazonaws.com",
want: &Registry{
ID: "",
FIPS: true,
Region: "us-west-1",
Public: false,
},
},
// Invalid endpoints
{
name: "not an ECR URL",
url: "not-an-ecr-url.com",
wantError: true,
},
{
name: "invalid account ID length",
url: "123.dkr.ecr-fips.us-east-1.amazonaws.com",
wantError: true,
},
{
name: "empty string",
url: "",
wantError: true,
},
{
name: "docker hub",
url: "docker.io",
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseECREndpoint(tt.url)
if tt.wantError {
if err == nil {
t.Errorf("ParseECREndpoint() expected error but got none")
}
return
}
if err != nil {
t.Errorf("ParseECREndpoint() unexpected error: %v", err)
return
}
if got.ID != tt.want.ID {
t.Errorf("ParseECREndpoint() ID = %v, want %v", got.ID, tt.want.ID)
}
if got.FIPS != tt.want.FIPS {
t.Errorf("ParseECREndpoint() FIPS = %v, want %v", got.FIPS, tt.want.FIPS)
}
if got.Region != tt.want.Region {
t.Errorf("ParseECREndpoint() Region = %v, want %v", got.Region, tt.want.Region)
}
if got.Public != tt.want.Public {
t.Errorf("ParseECREndpoint() Public = %v, want %v", got.Public, tt.want.Public)
}
})
}
}
+4 -3
View File
@@ -12,7 +12,6 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/portainer/portainer/api/logs"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
@@ -98,7 +97,7 @@ func encrypt(path string, passphrase string) (string, error) {
if err != nil {
return "", err
}
defer logs.CloseAndLogErr(in)
defer in.Close()
outFileName := path + ".encrypted"
out, err := os.Create(outFileName)
@@ -106,5 +105,7 @@ func encrypt(path string, passphrase string) (string, error) {
return "", err
}
return outFileName, crypto.AesEncrypt(in, out, []byte(passphrase))
err = crypto.AesEncrypt(in, out, []byte(passphrase))
return outFileName, err
}
+11 -19
View File
@@ -16,8 +16,6 @@ import (
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/offlinegate"
"github.com/rs/zerolog/log"
)
var filesToRestore = append(filesToBackup, "portainer.db")
@@ -33,20 +31,17 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
}
restorePath := filepath.Join(filestorePath, "restore", time.Now().Format("20060102150405"))
defer func() {
if err := os.RemoveAll(filepath.Dir(restorePath)); err != nil {
log.Warn().Err(err).Msg("failed to clean up restore files")
}
}()
defer os.RemoveAll(filepath.Dir(restorePath))
if err := extractArchive(archive, restorePath); err != nil {
err = extractArchive(archive, restorePath)
if err != nil {
return errors.Wrap(err, "cannot extract files from the archive. Please ensure the password is correct and try again")
}
unlock := gate.Lock()
defer unlock()
if err := datastore.Close(); err != nil {
if err = datastore.Close(); err != nil {
return errors.Wrap(err, "Failed to stop db")
}
@@ -56,7 +51,7 @@ func RestoreArchive(archive io.Reader, password string, filestorePath string, ga
return errors.Wrap(err, "failed to restore from backup. Portainer database missing from backup file")
}
if err := restoreFiles(restorePath, filestorePath); err != nil {
if err = restoreFiles(restorePath, filestorePath); err != nil {
return errors.Wrap(err, "failed to restore the system state")
}
@@ -94,7 +89,8 @@ func getRestoreSourcePath(dir string) (string, error) {
func restoreFiles(srcDir string, destinationDir string) error {
for _, filename := range filesToRestore {
if err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir); err != nil {
err := filesystem.CopyPath(filepath.Join(srcDir, filename), destinationDir)
if err != nil {
return err
}
}
@@ -102,18 +98,14 @@ func restoreFiles(srcDir string, destinationDir string) error {
// TODO: This is very boltdb module specific once again due to the filename. Move to bolt module? Refactor for another day
// Prevent the possibility of having both databases. Remove any default new instance
if err := os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName)); err != nil && !os.IsNotExist(err) {
return err
}
if err := os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName)); err != nil && !os.IsNotExist(err) {
return err
}
os.Remove(filepath.Join(destinationDir, boltdb.DatabaseFileName))
os.Remove(filepath.Join(destinationDir, boltdb.EncryptedDatabaseFileName))
// Now copy the database. It'll be either portainer.db or portainer.edb
// Note: CopyPath does not return an error if the source file doesn't exist
if err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir); err != nil {
err := filesystem.CopyPath(filepath.Join(srcDir, boltdb.EncryptedDatabaseFileName), destinationDir)
if err != nil {
return err
}
-1
View File
@@ -6,7 +6,6 @@ import (
)
func TestGenerateGo119CompatibleKey(t *testing.T) {
t.Parallel()
type args struct {
seed string
}
+19 -8
View File
@@ -11,7 +11,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/pkg/schedule"
chserver "github.com/jpillora/chisel/server"
"github.com/jpillora/chisel/share/ccrypto"
@@ -90,8 +89,10 @@ func (service *Service) pingAgent(endpointID portainer.EndpointID) error {
return err
}
_, _ = io.Copy(io.Discard, resp.Body)
return resp.Body.Close()
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
return nil
}
// KeepTunnelAlive keeps the tunnel of the given environment for maxAlive duration, or until ctx is done
@@ -234,13 +235,23 @@ func (service *Service) startTunnelVerificationLoop() {
Float64("check_interval_seconds", tunnelCleanupInterval.Seconds()).
Msg("starting tunnel management process")
schedule.RunOnInterval(service.shutdownCtx, tunnelCleanupInterval, service.checkTunnels, func() {
log.Debug().Msg("shutting down tunnel service")
ticker := time.NewTicker(tunnelCleanupInterval)
if err := service.StopTunnelServer(); err != nil {
log.Debug().Err(err).Msg("stopped tunnel service")
for {
select {
case <-ticker.C:
service.checkTunnels()
case <-service.shutdownCtx.Done():
log.Debug().Msg("shutting down tunnel service")
if err := service.StopTunnelServer(); err != nil {
log.Debug().Err(err).Msg("stopped tunnel service")
}
ticker.Stop()
return
}
})
}
}
// checkTunnels finds the first tunnel that has not had any activity recently
+3 -3
View File
@@ -1,6 +1,7 @@
package chisel
import (
"context"
"net"
"net/http"
"testing"
@@ -18,7 +19,6 @@ func init() {
}
func TestPingAgentPanic(t *testing.T) {
t.Parallel()
endpoint := &portainer.Endpoint{
ID: 1,
EdgeID: "test-edge-id",
@@ -26,7 +26,7 @@ func TestPingAgentPanic(t *testing.T) {
UserTrusted: true,
}
_, store := datastore.MustNewTestStore(t, false, true)
_, store := datastore.MustNewTestStore(t, true, true)
s := NewService(store, nil, nil)
@@ -54,6 +54,6 @@ func TestPingAgentPanic(t *testing.T) {
s.activeTunnels[endpoint.ID].Port = ln.Addr().(*net.TCPAddr).Port
require.Error(t, s.pingAgent(endpoint.ID))
require.NoError(t, srv.Shutdown(t.Context()))
require.NoError(t, srv.Shutdown(context.Background()))
require.ErrorIs(t, <-errCh, http.ErrServerClosed)
}
+1 -3
View File
@@ -142,9 +142,7 @@ func (s *Service) TunnelAddr(endpoint *portainer.Endpoint) (string, error) {
continue
}
if err := conn.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close tcp connection")
}
conn.Close()
break
}
-1
View File
@@ -28,7 +28,6 @@ func (s *testStore) Settings() dataservices.SettingsService {
}
func TestGetUnusedPort(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
existingTunnels map[portainer.EndpointID]*portainer.TunnelDetails
+6 -10
View File
@@ -32,7 +32,7 @@ func CLIFlags() *portainer.CLIFlags {
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: kingpin.Flag("feat", "List of feature flags").Envar(portainer.FeatureFlagEnvVar).Strings(),
FeatureFlags: kingpin.Flag("feat", "List of feature flags").Strings(),
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
@@ -52,6 +52,7 @@ func CLIFlags() *portainer.CLIFlags {
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
LogLevel: kingpin.Flag("log-level", "Set the minimum logging level to show").Default("INFO").Enum("DEBUG", "INFO", "WARN", "ERROR"),
LogMode: kingpin.Flag("log-mode", "Set the logging output mode").Default("PRETTY").Enum("NOCOLOR", "PRETTY", "JSON"),
KubectlShellImage: kingpin.Flag("kubectl-shell-image", "Kubectl shell image").Envar(portainer.KubectlShellImageEnvVar).Default(portainer.DefaultKubectlShellImage).String(),
PullLimitCheckDisabled: kingpin.Flag("pull-limit-check-disabled", "Pull limit check").Envar(portainer.PullLimitCheckDisabledEnvVar).Default(defaultPullLimitCheckDisabled).Bool(),
TrustedOrigins: kingpin.Flag("trusted-origins", "List of trusted origins for CSRF protection. Separate multiple origins with a comma.").Envar(portainer.TrustedOriginsEnvVar).String(),
CSP: kingpin.Flag("csp", "Content Security Policy (CSP) header").Envar(portainer.CSPEnvVar).Default("true").Bool(),
@@ -94,11 +95,6 @@ func (Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
flags.TLSKey = tlsKeyFlag.String()
flags.TLSCacert = kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String()
flags.KubectlShellImage = kingpin.Flag(
"kubectl-shell-image",
"Kubectl shell image",
).Envar(portainer.KubectlShellImageEnvVar).Default(portainer.DefaultKubectlShellImage).String()
kingpin.Parse()
if !filepath.IsAbs(*flags.Assets) {
@@ -152,11 +148,11 @@ func (Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
func (Service) ValidateFlags(flags *portainer.CLIFlags) error {
displayDeprecationWarnings(flags)
if err := ValidateEndpointURL(*flags.EndpointURL); err != nil {
if err := validateEndpointURL(*flags.EndpointURL); err != nil {
return err
}
if err := ValidateSnapshotInterval(*flags.SnapshotInterval); err != nil {
if err := validateSnapshotInterval(*flags.SnapshotInterval); err != nil {
return err
}
@@ -173,7 +169,7 @@ func displayDeprecationWarnings(flags *portainer.CLIFlags) {
}
}
func ValidateEndpointURL(endpointURL string) error {
func validateEndpointURL(endpointURL string) error {
if endpointURL == "" {
return nil
}
@@ -198,7 +194,7 @@ func ValidateEndpointURL(endpointURL string) error {
return nil
}
func ValidateSnapshotInterval(snapshotInterval string) error {
func validateSnapshotInterval(snapshotInterval string) error {
if snapshotInterval == "" {
return nil
}
+1
View File
@@ -1,4 +1,5 @@
//go:build !windows
// +build !windows
package cli
+33 -67
View File
@@ -7,7 +7,6 @@ import (
"os"
"path"
"strings"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/apikey"
@@ -52,10 +51,11 @@ import (
"github.com/portainer/portainer/pkg/featureflags"
"github.com/portainer/portainer/pkg/fips"
"github.com/portainer/portainer/pkg/libhelm"
libhelmtypes "github.com/portainer/portainer/pkg/libhelm/types"
"github.com/portainer/portainer/pkg/libstack/compose"
"github.com/portainer/portainer/pkg/validate"
"github.com/google/uuid"
"github.com/gofrs/uuid"
"github.com/rs/zerolog/log"
)
@@ -119,7 +119,7 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
if isNew {
instanceId, err := uuid.NewRandom()
instanceId, err := uuid.NewV4()
if err != nil {
log.Fatal().Err(err).Msg("failed generating instance id")
}
@@ -134,16 +134,15 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
InstanceID: instanceId.String(),
MigratorCount: migratorCount,
}
if err := store.VersionService.UpdateVersion(&v); err != nil {
log.Fatal().Err(err).Msg("failed to update version")
}
store.VersionService.UpdateVersion(&v)
if err := updateSettingsFromFlags(store, flags); err != nil {
log.Fatal().Err(err).Msg("failed updating settings from flags")
}
} else if err := store.MigrateData(); err != nil {
log.Fatal().Err(err).Msg("failed migration")
} else {
if err := store.MigrateData(); err != nil {
log.Fatal().Err(err).Msg("failed migration")
}
}
if err := updateSettingsFromFlags(store, flags); err != nil {
@@ -154,7 +153,7 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
go func() {
<-shutdownCtx.Done()
defer logs.CloseAndLogErr(connection)
defer connection.Close()
}()
return store
@@ -174,6 +173,10 @@ func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheMan
return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, proxyManager)
}
func initHelmPackageManager() (libhelmtypes.HelmPackageManager, error) {
return libhelm.NewHelmPackageManager()
}
func initAPIKeyService(datastore dataservices.DataStore) apikey.APIKeyService {
return apikey.NewAPIKeyService(datastore.APIKeyRepository(), datastore.User())
}
@@ -212,12 +215,13 @@ func initSnapshotService(
dataStore dataservices.DataStore,
dockerClientFactory *dockerclient.ClientFactory,
kubernetesClientFactory *kubecli.ClientFactory,
shutdownCtx context.Context,
pendingActionsService *pendingactions.PendingActionsService,
) (portainer.SnapshotService, error) {
dockerSnapshotter := docker.NewSnapshotter(dockerClientFactory)
kubernetesSnapshotter := kubernetes.NewSnapshotter(kubernetesClientFactory)
snapshotService, err := snapshot.NewService(snapshotIntervalFromFlag, dataStore, dockerSnapshotter, kubernetesSnapshotter, pendingActionsService)
snapshotService, err := snapshot.NewService(snapshotIntervalFromFlag, dataStore, dockerSnapshotter, kubernetesSnapshotter, shutdownCtx, pendingActionsService)
if err != nil {
return nil, err
}
@@ -333,7 +337,8 @@ func loadEncryptionSecretKey(keyfilename string) []byte {
return hash[:]
}
func buildServer(flags *portainer.CLIFlags, shutdownCtx context.Context, shutdownTrigger context.CancelFunc) portainer.Server {
func buildServer(flags *portainer.CLIFlags) portainer.Server {
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
if flags.FeatureFlags != nil {
featureflags.Parse(*flags.FeatureFlags, portainer.SupportedFeatureFlags)
@@ -342,9 +347,9 @@ func buildServer(flags *portainer.CLIFlags, shutdownCtx context.Context, shutdow
trustedOrigins := []string{}
if *flags.TrustedOrigins != "" {
// validate if the trusted origins are valid urls
for origin := range strings.SplitSeq(*flags.TrustedOrigins, ",") {
for _, origin := range strings.Split(*flags.TrustedOrigins, ",") {
if !validate.IsTrustedOrigin(origin) {
log.Fatal().Str("trusted_origin", origin).Msg("invalid trusted origin: must be scheme://host or scheme://host:port (e.g. https://example.com)")
log.Fatal().Str("trusted_origin", origin).Msg("invalid url for trusted origin. Please check the trusted origins flag.")
}
trustedOrigins = append(trustedOrigins, origin)
@@ -455,16 +460,19 @@ func buildServer(flags *portainer.CLIFlags, shutdownCtx context.Context, shutdow
pendingActionsService.RegisterHandler(actions.DeletePortainerK8sRegistrySecrets, handlers.NewHandlerDeleteRegistrySecrets(authorizationService, dataStore, kubernetesClientFactory))
pendingActionsService.RegisterHandler(actions.PostInitMigrateEnvironment, handlers.NewHandlerPostInitMigrateEnvironment(authorizationService, dataStore, kubernetesClientFactory, dockerClientFactory, *flags.Assets, kubernetesDeployer))
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, pendingActionsService)
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx, pendingActionsService)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing snapshot service")
}
snapshotService.Start(shutdownCtx)
snapshotService.Start()
proxyManager.NewProxyFactory(dataStore, signatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager, gitService, snapshotService, jwtService)
helmPackageManager := libhelm.NewHelmPackageManager()
helmPackageManager, err := initHelmPackageManager()
if err != nil {
log.Fatal().Err(err).Msg("failed initializing helm package manager")
}
applicationStatus := initStatus(instanceID)
@@ -521,16 +529,17 @@ func buildServer(flags *portainer.CLIFlags, shutdownCtx context.Context, shutdow
scheduler := scheduler.NewScheduler(shutdownCtx)
stackDeployer := deployments.NewStackDeployer(swarmStackManager, composeStackManager, kubernetesDeployer, dockerClientFactory, dataStore)
if err := deployments.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService); err != nil {
log.Fatal().Err(err).Msg("failed to start stack scheduler")
}
deployments.StartStackSchedules(scheduler, stackDeployer, dataStore, gitService)
sslDBSettings, err := dataStore.SSLSettings().Settings()
if err != nil {
log.Fatal().Msg("failed to fetch SSL settings from DB")
}
platformService := platform.NewService(dataStore)
platformService, err := platform.NewService(dataStore)
if err != nil {
log.Fatal().Err(err).Msg("failed initializing platform service")
}
upgradeService, err := upgrade.NewService(
*flags.Assets,
@@ -560,13 +569,6 @@ func buildServer(flags *portainer.CLIFlags, shutdownCtx context.Context, shutdow
log.Fatal().Err(err).Msg("failure during post init migrations")
}
if err := dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
return recoverStaleDeployingStacks(tx)
}); err != nil {
log.Info().Err(err).
Msg("Error recovering stale deploying stacks")
}
return &http.Server{
AuthorizationService: authorizationService,
ReverseTunnelService: reverseTunnelService,
@@ -599,6 +601,7 @@ func buildServer(flags *portainer.CLIFlags, shutdownCtx context.Context, shutdow
DockerClientFactory: dockerClientFactory,
KubernetesClientFactory: kubernetesClientFactory,
Scheduler: scheduler,
ShutdownCtx: shutdownCtx,
ShutdownTrigger: shutdownTrigger,
StackDeployer: stackDeployer,
UpgradeService: upgradeService,
@@ -620,8 +623,7 @@ func main() {
logs.SetLoggingMode(*flags.LogMode)
for {
shutdownCtx, shutdownTrigger := context.WithCancel(context.Background())
server := buildServer(flags, shutdownCtx, shutdownTrigger)
server := buildServer(flags)
log.Info().
Str("version", portainer.APIVersion).
@@ -633,44 +635,8 @@ func main() {
Str("go_version", build.GoVersion).
Msg("starting Portainer")
err := server.Start(shutdownCtx)
err := server.Start()
log.Info().Err(err).Msg("HTTP server exited")
}
}
// recoverStaleDeployingStacks resets any stack that was left in the Deploying state
// (e.g. because the server was restarted mid-deployment) to the Error state so the
// user can retry.
func recoverStaleDeployingStacks(tx dataservices.DataStoreTx) error {
stacks, err := tx.Stack().ReadAll(func(s portainer.Stack) bool {
return s.Status == portainer.StackStatusDeploying
})
if err != nil {
return err
}
for _, stack := range stacks {
stack.Status = portainer.StackStatusError
stack.DeploymentStatus = append(stack.DeploymentStatus, portainer.StackDeploymentStatus{
Status: portainer.StackStatusError,
Time: time.Now().Unix(),
Message: "Deployment interrupted by server restart",
})
if err := tx.Stack().Update(stack.ID, &stack); err != nil {
log.Warn().Err(err).
Int("stack_id", int(stack.ID)).
Str("context", "RecoverStaleDeployingStacks").
Msg("Unable to recover stale deploying stack")
continue
}
log.Debug().
Int("stack_id", int(stack.ID)).
Str("stack_name", stack.Name).
Str("context", "RecoverStaleDeployingStacks").
Msg("Recovered stale deploying stack to error state")
}
return nil
}
+2 -5
View File
@@ -2,10 +2,9 @@ package main
import (
"os"
"path"
"testing"
"github.com/portainer/portainer/api/filesystem"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -19,9 +18,8 @@ func createPasswordFile(t *testing.T, secretPath, password string) string {
}
func TestLoadEncryptionSecretKey(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
secretPath := filesystem.JoinPaths(tempDir, secretFileName)
secretPath := path.Join(tempDir, secretFileName)
// first pointing to file that does not exist, gives nil hash (no encryption)
encryptionKey := loadEncryptionSecretKey(secretPath)
@@ -41,7 +39,6 @@ func TestLoadEncryptionSecretKey(t *testing.T) {
}
func TestDBSecretPath(t *testing.T) {
t.Parallel()
tests := []struct {
keyFilenameFlag string
expected string
+4 -9
View File
@@ -15,9 +15,8 @@ import (
"github.com/portainer/portainer/pkg/fips"
// Not allowed in FIPS mode
"golang.org/x/crypto/argon2" //nolint:depguard
"golang.org/x/crypto/scrypt" //nolint:depguard
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/scrypt"
)
const (
@@ -164,9 +163,7 @@ func aesEncryptGCM(input io.Reader, output io.Writer, passphrase []byte) error {
return err
}
if err := nonce.Increment(); err != nil {
return err
}
nonce.Increment()
}
return nil
@@ -237,9 +234,7 @@ func aesDecryptGCM(input io.Reader, passphrase []byte) (io.Reader, error) {
return nil, err
}
if err := nonce.Increment(); err != nil {
return nil, err
}
nonce.Increment()
}
return &buf, nil
+85 -123
View File
@@ -6,10 +6,9 @@ import (
"io"
"math/rand"
"os"
"path/filepath"
"testing"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/logs"
"github.com/portainer/portainer/pkg/fips"
"github.com/stretchr/testify/assert"
@@ -42,35 +41,32 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
tmpdir := t.TempDir()
var (
originFilePath = filesystem.JoinPaths(tmpdir, "origin")
encryptedFilePath = filesystem.JoinPaths(tmpdir, "encrypted")
decryptedFilePath = filesystem.JoinPaths(tmpdir, "decrypted")
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := randBytes(1024*1024*100 + 523)
err := os.WriteFile(originFilePath, content, 0600)
require.NoError(t, err)
os.WriteFile(originFilePath, content, 0600)
originFile, _ := os.Open(originFilePath)
defer logs.CloseAndLogErr(originFile)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
err = encrypt(originFile, encryptedFileWriter, []byte(passphrase))
require.NoError(t, err, "Failed to encrypt a file")
logs.CloseAndLogErr(encryptedFileWriter)
err := encrypt(originFile, encryptedFileWriter, []byte(passphrase))
require.Nil(t, err, "Failed to encrypt a file")
encryptedFileWriter.Close()
encryptedContent, err := os.ReadFile(encryptedFilePath)
require.NoError(t, err, "Couldn't read encrypted file")
require.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, err := os.Open(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileReader)
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, err := os.Create(decryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(decryptedFileWriter)
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := decrypt(encryptedFileReader, []byte(passphrase))
if !decryptShouldSucceed {
@@ -78,11 +74,9 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
} else {
require.NoError(t, err, "Failed to decrypt file indicated by decryptShouldSucceed")
_, err = io.Copy(decryptedFileWriter, decryptedReader)
require.NoError(t, err)
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, err := os.ReadFile(decryptedFilePath)
require.NoError(t, err)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
}
@@ -141,53 +135,45 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
}
func Test_encryptAndDecrypt_withStrongPassphrase(t *testing.T) {
t.Parallel()
const passphrase = "A strong passphrase with special characters: !@#$%^&*()_+"
testFunc := func(t *testing.T, encrypt encryptFunc, decrypt decryptFunc) {
tmpdir := t.TempDir()
var (
originFilePath = filesystem.JoinPaths(tmpdir, "origin2")
encryptedFilePath = filesystem.JoinPaths(tmpdir, "encrypted2")
decryptedFilePath = filesystem.JoinPaths(tmpdir, "decrypted2")
originFilePath = filepath.Join(tmpdir, "origin2")
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
)
content := randBytes(500)
os.WriteFile(originFilePath, content, 0600)
err := os.WriteFile(originFilePath, content, 0600)
require.NoError(t, err)
originFile, err := os.Open(originFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(originFile)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, _ := os.Create(encryptedFilePath)
err = encrypt(originFile, encryptedFileWriter, []byte(passphrase))
require.NoError(t, err, "Failed to encrypt a file")
logs.CloseAndLogErr(encryptedFileWriter)
err := encrypt(originFile, encryptedFileWriter, []byte(passphrase))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedFileWriter.Close()
encryptedContent, err := os.ReadFile(encryptedFilePath)
require.NoError(t, err, "Couldn't read encrypted file")
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, err := os.Open(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileReader)
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, err := os.Create(decryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(decryptedFileWriter)
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := decrypt(encryptedFileReader, []byte(passphrase))
require.NoError(t, err, "Failed to decrypt file")
assert.Nil(t, err, "Failed to decrypt file")
_, err = io.Copy(decryptedFileWriter, decryptedReader)
require.NoError(t, err)
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, err := os.ReadFile(decryptedFilePath)
require.NoError(t, err)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
@@ -201,51 +187,43 @@ func Test_encryptAndDecrypt_withStrongPassphrase(t *testing.T) {
}
func Test_encryptAndDecrypt_withTheSamePasswordSmallFile(t *testing.T) {
t.Parallel()
testFunc := func(t *testing.T, encrypt encryptFunc, decrypt decryptFunc) {
tmpdir := t.TempDir()
var (
originFilePath = filesystem.JoinPaths(tmpdir, "origin2")
encryptedFilePath = filesystem.JoinPaths(tmpdir, "encrypted2")
decryptedFilePath = filesystem.JoinPaths(tmpdir, "decrypted2")
originFilePath = filepath.Join(tmpdir, "origin2")
encryptedFilePath = filepath.Join(tmpdir, "encrypted2")
decryptedFilePath = filepath.Join(tmpdir, "decrypted2")
)
content := randBytes(500)
err := os.WriteFile(originFilePath, content, 0600)
require.NoError(t, err)
os.WriteFile(originFilePath, content, 0600)
originFile, err := os.Open(originFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(originFile)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, err := os.Create(encryptedFilePath)
require.NoError(t, err)
encryptedFileWriter, _ := os.Create(encryptedFilePath)
err = encrypt(originFile, encryptedFileWriter, []byte("passphrase"))
require.NoError(t, err, "Failed to encrypt a file")
logs.CloseAndLogErr(encryptedFileWriter)
err := encrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedFileWriter.Close()
encryptedContent, err := os.ReadFile(encryptedFilePath)
require.NoError(t, err, "Couldn't read encrypted file")
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, err := os.Open(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileReader)
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, err := os.Create(decryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(decryptedFileWriter)
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := decrypt(encryptedFileReader, []byte("passphrase"))
require.NoError(t, err, "Failed to decrypt file")
assert.Nil(t, err, "Failed to decrypt file")
_, err = io.Copy(decryptedFileWriter, decryptedReader)
require.NoError(t, err)
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, err := os.ReadFile(decryptedFilePath)
require.NoError(t, err)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
@@ -259,51 +237,42 @@ func Test_encryptAndDecrypt_withTheSamePasswordSmallFile(t *testing.T) {
}
func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
t.Parallel()
testFunc := func(t *testing.T, encrypt encryptFunc, decrypt decryptFunc) {
tmpdir := t.TempDir()
var (
originFilePath = filesystem.JoinPaths(tmpdir, "origin")
encryptedFilePath = filesystem.JoinPaths(tmpdir, "encrypted")
decryptedFilePath = filesystem.JoinPaths(tmpdir, "decrypted")
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := randBytes(1024 * 50)
err := os.WriteFile(originFilePath, content, 0600)
require.NoError(t, err)
os.WriteFile(originFilePath, content, 0600)
originFile, err := os.Open(originFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(originFile)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, err := os.Create(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileWriter)
err = encrypt(originFile, encryptedFileWriter, []byte(""))
require.NoError(t, err, "Failed to encrypt a file")
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err := encrypt(originFile, encryptedFileWriter, []byte(""))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := os.ReadFile(encryptedFilePath)
require.NoError(t, err, "Couldn't read encrypted file")
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, err := os.Open(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileReader)
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, err := os.Create(decryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(decryptedFileWriter)
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
decryptedReader, err := decrypt(encryptedFileReader, []byte(""))
require.NoError(t, err, "Failed to decrypt file")
assert.Nil(t, err, "Failed to decrypt file")
_, err = io.Copy(decryptedFileWriter, decryptedReader)
require.NoError(t, err)
io.Copy(decryptedFileWriter, decryptedReader)
decryptedContent, err := os.ReadFile(decryptedFilePath)
require.NoError(t, err)
decryptedContent, _ := os.ReadFile(decryptedFilePath)
assert.Equal(t, content, decryptedContent, "Original and decrypted content should match")
}
@@ -317,44 +286,38 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
}
func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T) {
t.Parallel()
testFunc := func(t *testing.T, encrypt encryptFunc, decrypt decryptFunc) {
tmpdir := t.TempDir()
var (
originFilePath = filesystem.JoinPaths(tmpdir, "origin")
encryptedFilePath = filesystem.JoinPaths(tmpdir, "encrypted")
decryptedFilePath = filesystem.JoinPaths(tmpdir, "decrypted")
originFilePath = filepath.Join(tmpdir, "origin")
encryptedFilePath = filepath.Join(tmpdir, "encrypted")
decryptedFilePath = filepath.Join(tmpdir, "decrypted")
)
content := randBytes(1034)
err := os.WriteFile(originFilePath, content, 0600)
require.NoError(t, err)
os.WriteFile(originFilePath, content, 0600)
originFile, err := os.Open(originFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(originFile)
originFile, _ := os.Open(originFilePath)
defer originFile.Close()
encryptedFileWriter, err := os.Create(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileWriter)
encryptedFileWriter, _ := os.Create(encryptedFilePath)
defer encryptedFileWriter.Close()
err = encrypt(originFile, encryptedFileWriter, []byte("passphrase"))
require.NoError(t, err, "Failed to encrypt a file")
err := encrypt(originFile, encryptedFileWriter, []byte("passphrase"))
assert.Nil(t, err, "Failed to encrypt a file")
encryptedContent, err := os.ReadFile(encryptedFilePath)
require.NoError(t, err, "Couldn't read encrypted file")
assert.Nil(t, err, "Couldn't read encrypted file")
assert.NotEqual(t, encryptedContent, content, "Content wasn't encrypted")
encryptedFileReader, err := os.Open(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileReader)
encryptedFileReader, _ := os.Open(encryptedFilePath)
defer encryptedFileReader.Close()
decryptedFileWriter, err := os.Create(decryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(decryptedFileWriter)
decryptedFileWriter, _ := os.Create(decryptedFilePath)
defer decryptedFileWriter.Close()
_, err = decrypt(encryptedFileReader, []byte("garbage"))
require.Error(t, err, "Should not allow decrypt with wrong passphrase")
assert.NotNil(t, err, "Should not allow decrypt with wrong passphrase")
}
t.Run("fips", func(t *testing.T) {
@@ -389,7 +352,6 @@ func legacyAesEncrypt(input io.Reader, output io.Writer, passphrase []byte) erro
}
func Test_hasEncryptedHeader(t *testing.T) {
t.Parallel()
tests := []struct {
name string
data []byte
+3 -4
View File
@@ -7,17 +7,16 @@ import (
)
func TestCreateSignature(t *testing.T) {
t.Parallel()
var s = NewECDSAService("secret")
privKey, pubKey, err := s.GenerateKeyPair()
require.NoError(t, err)
require.NotEmpty(t, privKey)
require.NotEmpty(t, pubKey)
require.Greater(t, len(privKey), 0)
require.Greater(t, len(pubKey), 0)
m := "test message"
r, err := s.CreateSignature(m)
require.NoError(t, err)
require.NotEqual(t, r, m)
require.NotEmpty(t, r)
require.Greater(t, len(r), 0)
}
+1 -2
View File
@@ -1,8 +1,7 @@
package crypto
import (
// Not allowed in FIPS mode
"golang.org/x/crypto/bcrypt" //nolint:depguard
"golang.org/x/crypto/bcrypt"
)
// Service represents a service for encrypting/hashing data.
-2
View File
@@ -7,7 +7,6 @@ import (
)
func TestService_Hash(t *testing.T) {
t.Parallel()
var s = Service{}
type args struct {
@@ -56,7 +55,6 @@ func TestService_Hash(t *testing.T) {
}
func TestHash(t *testing.T) {
t.Parallel()
s := Service{}
hash, err := s.Hash("Passw0rd!")
+1 -3
View File
@@ -92,9 +92,7 @@ func CreateTLSConfigurationFromDisk(config portainer.TLSConfiguration) (*tls.Con
}
func createTLSConfigurationFromDisk(fipsEnabled bool, config portainer.TLSConfiguration) (*tls.Config, error) { //nolint:forbidigo
if !config.TLS && fipsEnabled {
return nil, fips.ErrTLSRequired
} else if !config.TLS {
if !config.TLS {
return nil, nil
}
+2 -7
View File
@@ -10,7 +10,6 @@ import (
)
func TestCreateTLSConfiguration(t *testing.T) {
t.Parallel()
// InsecureSkipVerify = false
config := CreateTLSConfiguration(false)
require.Equal(t, config.MinVersion, uint16(tls.VersionTLS12)) //nolint:forbidigo
@@ -23,7 +22,6 @@ func TestCreateTLSConfiguration(t *testing.T) {
}
func TestCreateTLSConfigurationFIPS(t *testing.T) {
t.Parallel()
fips := true
fipsCipherSuites := []uint16{
@@ -44,10 +42,9 @@ func TestCreateTLSConfigurationFIPS(t *testing.T) {
}
func TestCreateTLSConfigurationFromBytes(t *testing.T) {
t.Parallel()
// No TLS
config, err := CreateTLSConfigurationFromBytes(false, nil, nil, nil, false, false)
require.NoError(t, err)
require.Nil(t, err)
require.Nil(t, config)
// Skip TLS client/server verifications
@@ -62,10 +59,9 @@ func TestCreateTLSConfigurationFromBytes(t *testing.T) {
}
func TestCreateTLSConfigurationFromDisk(t *testing.T) {
t.Parallel()
// No TLS
config, err := CreateTLSConfigurationFromDisk(portainer.TLSConfiguration{})
require.NoError(t, err)
require.Nil(t, err)
require.Nil(t, config)
// Skip TLS verifications
@@ -78,7 +74,6 @@ func TestCreateTLSConfigurationFromDisk(t *testing.T) {
}
func TestCreateTLSConfigurationFromDiskFIPS(t *testing.T) {
t.Parallel()
fips := true
// Skipping TLS verifications cannot be done in FIPS mode
+11 -31
View File
@@ -2,6 +2,7 @@ package boltdb
import (
"os"
"path"
"testing"
"github.com/portainer/portainer/api/filesystem"
@@ -12,7 +13,6 @@ import (
)
func Test_NeedsEncryptionMigration(t *testing.T) {
t.Parallel()
// Test the specific scenarios mentioned in NeedsEncryptionMigration
// i.e.
@@ -96,38 +96,20 @@ func Test_NeedsEncryptionMigration(t *testing.T) {
if tc.dbname == "both" {
// Special case. If portainer.db and portainer.edb exist.
dbFile1 := filesystem.JoinPaths(connection.Path, DatabaseFileName)
dbFile1 := path.Join(connection.Path, DatabaseFileName)
f, _ := os.Create(dbFile1)
f.Close()
defer os.Remove(dbFile1)
err := f.Close()
require.NoError(t, err)
defer func() {
err := os.Remove(dbFile1)
require.NoError(t, err)
}()
dbFile2 := filesystem.JoinPaths(connection.Path, EncryptedDatabaseFileName)
dbFile2 := path.Join(connection.Path, EncryptedDatabaseFileName)
f, _ = os.Create(dbFile2)
err = f.Close()
require.NoError(t, err)
defer func() {
err := os.Remove(dbFile2)
require.NoError(t, err)
}()
f.Close()
defer os.Remove(dbFile2)
} else if tc.dbname != "" {
dbFile := filesystem.JoinPaths(connection.Path, tc.dbname)
dbFile := path.Join(connection.Path, tc.dbname)
f, _ := os.Create(dbFile)
err := f.Close()
require.NoError(t, err)
defer func() {
err := os.Remove(dbFile)
require.NoError(t, err)
}()
f.Close()
defer os.Remove(dbFile)
}
if tc.key {
@@ -143,7 +125,6 @@ func Test_NeedsEncryptionMigration(t *testing.T) {
}
func TestDBCompaction(t *testing.T) {
t.Parallel()
db := &DbConnection{Path: t.TempDir()}
err := db.Open()
@@ -155,8 +136,7 @@ func TestDBCompaction(t *testing.T) {
return err
}
err = b.Put([]byte("key"), []byte("value"))
require.NoError(t, err)
b.Put([]byte("key"), []byte("value"))
return nil
})
+1 -2
View File
@@ -3,7 +3,6 @@ package boltdb
import (
"time"
"github.com/portainer/portainer/api/logs"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
bolt "go.etcd.io/bbolt"
@@ -38,7 +37,7 @@ func (c *DbConnection) ExportJSON(databasePath string, metadata bool) ([]byte, e
if err != nil {
return []byte("{}"), err
}
defer logs.CloseAndLogErr(connection)
defer connection.Close()
backup := make(map[string]any)
if metadata {
+7 -11
View File
@@ -10,14 +10,14 @@ import (
"io"
"testing"
"github.com/google/uuid"
"github.com/gofrs/uuid"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
jsonobject = `{"LogoURL":"","BlackListedLabels":[],"AuthenticationMethod":1,"InternalAuthSettings": {"RequiredPasswordLength": 12}"LDAPSettings":{"AnonymousMode":true,"ReaderDN":"","URL":"","TLSConfig":{"TLS":false,"TLSSkipVerify":false},"StartTLS":false,"SearchSettings":[{"BaseDN":"","Filter":"","UserNameAttribute":""}],"GroupSearchSettings":[{"GroupBaseDN":"","GroupFilter":"","GroupAttribute":""}],"AutoCreateUsers":true},"OAuthSettings":{"ClientID":"","AccessTokenURI":"","AuthorizationURI":"","ResourceURI":"","RedirectURI":"","UserIdentifier":"","Scopes":"","OAuthAutoCreateUsers":false,"DefaultTeamID":0,"SSO":true,"LogoutURI":"","KubeSecretKey":"j0zLVtY/lAWBk62ByyF0uP80SOXaitsABP0TTJX8MhI="},"OpenAMTConfiguration":{"Enabled":false,"MPSServer":"","MPSUser":"","MPSPassword":"","MPSToken":"","CertFileContent":"","CertFileName":"","CertFilePassword":"","DomainName":""},"FeatureFlagSettings":{},"SnapshotInterval":"5m","TemplatesURL":"https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json","EdgeAgentCheckinInterval":5,"EnableEdgeComputeFeatures":false,"UserSessionTimeout":"8h","KubeconfigExpiry":"0","EnableTelemetry":true,"HelmRepositoryURL":"https://charts.bitnami.com/bitnami","KubectlShellImage":"portainer/kubectl-shell","DisplayDonationHeader":false,"DisplayExternalContributors":false,"EnableHostManagementFeatures":false,"AllowVolumeBrowserForRegularUsers":false,"AllowBindMountsForRegularUsers":false,"AllowPrivilegedModeForRegularUsers":false,"AllowHostNamespaceForRegularUsers":false,"AllowStackManagementForRegularUsers":false,"AllowDeviceMappingForRegularUsers":false,"AllowContainerCapabilitiesForRegularUsers":false}`
passphrase = "my secret key"
)
@@ -27,10 +27,9 @@ func secretToEncryptionKey(passphrase string) []byte {
}
func Test_MarshalObjectUnencrypted(t *testing.T) {
t.Parallel()
is := assert.New(t)
uuid := uuid.New()
uuid := uuid.Must(uuid.NewV4())
tests := []struct {
object any
@@ -95,14 +94,13 @@ func Test_MarshalObjectUnencrypted(t *testing.T) {
for _, test := range tests {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
data, err := conn.MarshalObject(test.object)
require.NoError(t, err)
is.NoError(err)
is.Equal(test.expected, string(data))
})
}
}
func Test_UnMarshalObjectUnencrypted(t *testing.T) {
t.Parallel()
is := assert.New(t)
// Based on actual data entering and what we expect out of the function
@@ -137,14 +135,13 @@ func Test_UnMarshalObjectUnencrypted(t *testing.T) {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
var object string
err := conn.UnmarshalObject(test.object, &object)
require.NoError(t, err)
is.NoError(err)
is.Equal(test.expected, object)
})
}
}
func Test_ObjectMarshallingEncrypted(t *testing.T) {
t.Parallel()
is := assert.New(t)
// Based on actual data entering and what we expect out of the function
@@ -175,19 +172,18 @@ func Test_ObjectMarshallingEncrypted(t *testing.T) {
t.Run(fmt.Sprintf("%s -> %s", test.object, test.expected), func(t *testing.T) {
data, err := conn.MarshalObject(test.object)
require.NoError(t, err)
is.NoError(err)
var object []byte
err = conn.UnmarshalObject(data, &object)
require.NoError(t, err)
is.NoError(err)
is.Equal(test.object, object)
})
}
}
func Test_NonceSources(t *testing.T) {
t.Parallel()
// ensure that the new go 1.24 NewGCMWithRandomNonce works correctly with
// the old way of creating and including the nonce
+41 -20
View File
@@ -6,7 +6,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/stretchr/testify/require"
)
const testBucketName = "test-bucket"
@@ -18,56 +17,70 @@ type testStruct struct {
}
func TestTxs(t *testing.T) {
t.Parallel()
conn := DbConnection{Path: t.TempDir()}
conn := DbConnection{
Path: t.TempDir(),
}
err := conn.Open()
require.NoError(t, err)
defer func() {
err := conn.Close()
require.NoError(t, err)
}()
if err != nil {
t.Fatal(err)
}
defer conn.Close()
// Error propagation
err = conn.UpdateTx(func(tx portainer.Transaction) error {
return errors.New("this is an error")
})
require.Error(t, err)
if err == nil {
t.Fatal("an error was expected, got nil instead")
}
// Create an object
newObj := testStruct{Key: "key", Value: "value"}
newObj := testStruct{
Key: "key",
Value: "value",
}
err = conn.UpdateTx(func(tx portainer.Transaction) error {
if err := tx.SetServiceName(testBucketName); err != nil {
err = tx.SetServiceName(testBucketName)
if err != nil {
return err
}
return tx.CreateObjectWithId(testBucketName, testId, newObj)
})
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
obj := testStruct{}
err = conn.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
})
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
if obj.Key != newObj.Key || obj.Value != newObj.Value {
t.Fatalf("expected %s:%s, got %s:%s instead", newObj.Key, newObj.Value, obj.Key, obj.Value)
}
// Update an object
updatedObj := testStruct{Key: "updated-key", Value: "updated-value"}
updatedObj := testStruct{
Key: "updated-key",
Value: "updated-value",
}
err = conn.UpdateTx(func(tx portainer.Transaction) error {
return tx.UpdateObject(testBucketName, conn.ConvertToKey(testId), &updatedObj)
})
require.NoError(t, err)
err = conn.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
})
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
if obj.Key != updatedObj.Key || obj.Value != updatedObj.Value {
t.Fatalf("expected %s:%s, got %s:%s instead", updatedObj.Key, updatedObj.Value, obj.Key, obj.Value)
@@ -77,12 +90,16 @@ func TestTxs(t *testing.T) {
err = conn.UpdateTx(func(tx portainer.Transaction) error {
return tx.DeleteObject(testBucketName, conn.ConvertToKey(testId))
})
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
err = conn.ViewTx(func(tx portainer.Transaction) error {
return tx.GetObject(testBucketName, conn.ConvertToKey(testId), &obj)
})
require.True(t, dataservices.IsErrObjectNotFound(err))
if !dataservices.IsErrObjectNotFound(err) {
t.Fatal(err)
}
// Get next identifier
err = conn.UpdateTx(func(tx portainer.Transaction) error {
@@ -95,11 +112,15 @@ func TestTxs(t *testing.T) {
return nil
})
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
// Try to write in a read transaction
err = conn.ViewTx(func(tx portainer.Transaction) error {
return tx.CreateObjectWithId(testBucketName, testId, newObj)
})
require.Error(t, err)
if err == nil {
t.Fatal("an error was expected, got nil instead")
}
}
-25
View File
@@ -1,25 +0,0 @@
package database
import (
"testing"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/filesystem"
"github.com/stretchr/testify/require"
)
func TestNewDatabase(t *testing.T) {
t.Parallel()
dbPath := filesystem.JoinPaths(t.TempDir(), "test.db")
connection, err := NewDatabase("boltdb", dbPath, nil, false)
require.NoError(t, err)
require.NotNil(t, connection)
_, ok := connection.(*boltdb.DbConnection)
require.True(t, ok)
connection, err = NewDatabase("unknown", dbPath, nil, false)
require.Error(t, err)
require.Nil(t, connection)
}
+2 -2
View File
@@ -21,7 +21,7 @@ type mockConnection struct {
portainer.Connection
}
func (m mockConnection) UpdateObject(bucket string, key []byte, value any) error {
func (m mockConnection) UpdateObject(bucket string, key []byte, value interface{}) error {
obj := value.(*testObject)
m.store[obj.ID] = *obj
@@ -50,8 +50,8 @@ func (m mockConnection) ViewTx(fn func(portainer.Transaction) error) error {
func (m mockConnection) ConvertToKey(v int) []byte {
return []byte(strconv.Itoa(v))
}
func TestReadAll(t *testing.T) {
t.Parallel()
service := BaseDataService[testObject, int]{
Bucket: "testBucket",
Connection: mockConnection{store: make(map[int]testObject)},
-10
View File
@@ -72,13 +72,3 @@ func (service BaseDataServiceTx[T, I]) Delete(ID I) error {
identifier := service.Connection.ConvertToKey(int(ID))
return service.Tx.DeleteObject(service.Bucket, identifier)
}
func Read[T any](tx portainer.Transaction, bucket string, key []byte) (*T, error) {
var element T
if err := tx.GetObject(bucket, key, &element); err != nil {
return nil, err
}
return &element, nil
}
@@ -9,8 +9,7 @@ import (
)
func TestCustomTemplateCreate(t *testing.T) {
t.Parallel()
_, ds := datastore.MustNewTestStore(t, false, false)
_, ds := datastore.MustNewTestStore(t, true, false)
require.NotNil(t, ds)
require.NoError(t, ds.CustomTemplate().Create(&portainer.CustomTemplate{ID: 1}))
+1 -2
View File
@@ -10,8 +10,7 @@ import (
)
func TestCustomTemplateCreateTx(t *testing.T) {
t.Parallel()
_, ds := datastore.MustNewTestStore(t, false, false)
_, ds := datastore.MustNewTestStore(t, true, false)
require.NotNil(t, ds)
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
+1 -3
View File
@@ -5,18 +5,16 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/logs"
"github.com/stretchr/testify/require"
)
func TestUpdate(t *testing.T) {
t.Parallel()
var conn portainer.Connection = &boltdb.DbConnection{Path: t.TempDir()}
err := conn.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn)
defer conn.Close()
service, err := NewService(conn, func(portainer.Transaction, portainer.EdgeStackID) {})
require.NoError(t, err)
-13
View File
@@ -119,19 +119,6 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
return endpoints, nil
}
// ReadAll retrieves all the elements that satisfy all the provided predicates.
func (service *Service) ReadAll(predicates ...func(endpoint portainer.Endpoint) bool) ([]portainer.Endpoint, error) {
var endpoints []portainer.Endpoint
var err error
err = service.connection.ViewTx(func(tx portainer.Transaction) error {
endpoints, err = service.Tx(tx).ReadAll(predicates...)
return err
})
return endpoints, err
}
// EndpointIDByEdgeID returns the EndpointID from the given EdgeID using an in-memory index
func (service *Service) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
service.mu.RLock()
-5
View File
@@ -89,11 +89,6 @@ func (service ServiceTx) Endpoints() ([]portainer.Endpoint, error) {
)
}
// ReadAll retrieves all the elements that satisfy all the provided predicates.
func (service ServiceTx) ReadAll(predicates ...func(endpoint portainer.Endpoint) bool) ([]portainer.Endpoint, error) {
return dataservices.BaseDataServiceTx[portainer.Endpoint, portainer.EndpointID]{Bucket: BucketName, Connection: service.service.connection, Tx: service.tx}.ReadAll(predicates...)
}
func (service ServiceTx) EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool) {
log.Error().Str("func", "EndpointIDByEdgeID").Msg("cannot be called inside a transaction")
@@ -28,9 +28,6 @@ func (service *Service) BucketName() string {
func (service *Service) RegisterUpdateStackFunction(
updateFuncTx func(portainer.Transaction, portainer.EdgeStackID, func(*portainer.EdgeStack)) error,
) {
service.mu.Lock()
defer service.mu.Unlock()
service.updateStackFnTx = updateFuncTx
}
@@ -7,13 +7,11 @@ import (
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/dataservices/edgestack"
"github.com/portainer/portainer/api/internal/edge/cache"
"github.com/portainer/portainer/api/logs"
"github.com/stretchr/testify/require"
)
func TestUpdateRelation(t *testing.T) {
t.Parallel()
const endpointID = 1
const edgeStackID1 = 1
const edgeStackID2 = 2
@@ -22,7 +20,7 @@ func TestUpdateRelation(t *testing.T) {
err := conn.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn)
defer conn.Close()
service, err := NewService(conn)
require.NoError(t, err)
@@ -107,12 +105,11 @@ func TestUpdateRelation(t *testing.T) {
}
func TestAddEndpointRelationsForEdgeStack(t *testing.T) {
t.Parallel()
var conn portainer.Connection = &boltdb.DbConnection{Path: t.TempDir()}
err := conn.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn)
defer conn.Close()
service, err := NewService(conn)
require.NoError(t, err)
@@ -127,12 +124,11 @@ func TestAddEndpointRelationsForEdgeStack(t *testing.T) {
}
func TestEndpointRelations(t *testing.T) {
t.Parallel()
var conn portainer.Connection = &boltdb.DbConnection{Path: t.TempDir()}
err := conn.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn)
defer conn.Close()
service, err := NewService(conn)
require.NoError(t, err)
@@ -140,5 +136,5 @@ func TestEndpointRelations(t *testing.T) {
require.NoError(t, service.Create(&portainer.EndpointRelation{EndpointID: 1}))
rels, err := service.EndpointRelations()
require.NoError(t, err)
require.Len(t, rels, 1)
require.Equal(t, 1, len(rels))
}
+1 -1
View File
@@ -6,7 +6,7 @@ import (
var (
ErrObjectNotFound = errors.New("object not found inside the database")
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://docs.portainer.io/faqs/upgrading/can-i-downgrade-from-portainer-business-to-portainer-ce")
ErrWrongDBEdition = errors.New("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
ErrDBImportFailed = errors.New("importing backup failed")
ErrDatabaseIsUpdating = errors.New("database is currently in updating state. Failed prior upgrade. Please restore from backup or delete the database and restart Portainer")
)
-4
View File
@@ -102,9 +102,6 @@ type (
// EndpointService represents a service for managing environment(endpoint) data
EndpointService interface {
// partial dataservices.BaseCRUD[portainer.Endpoint, portainer.EndpointID]
ReadAll(predicates ...func(endpoint portainer.Endpoint) bool) ([]portainer.Endpoint, error)
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error)
@@ -226,7 +223,6 @@ type (
UserService interface {
BaseCRUD[portainer.User, portainer.UserID]
UserByUsername(username string) (*portainer.User, error)
UserIDByUsername(username string) (portainer.UserID, error)
UsersByRole(role portainer.UserRole) ([]portainer.User, error)
}
@@ -10,7 +10,6 @@ import (
)
func TestDeleteByEndpoint(t *testing.T) {
t.Parallel()
_, store := datastore.MustNewTestStore(t, false, false)
// Create Endpoint 1
@@ -3,7 +3,6 @@ package resourcecontrol
import (
"errors"
"fmt"
"slices"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
@@ -65,9 +64,11 @@ func (service *Service) ResourceControlByResourceIDAndType(resourceID string, re
return nil, stop
}
if slices.Contains(rc.SubResourceIDs, resourceID) {
resourceControl = rc
return nil, stop
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = rc
return nil, stop
}
}
return &portainer.ResourceControl{}, nil
+5 -4
View File
@@ -3,7 +3,6 @@ package resourcecontrol
import (
"errors"
"fmt"
"slices"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
@@ -36,9 +35,11 @@ func (service ServiceTx) ResourceControlByResourceIDAndType(resourceID string, r
return nil, stop
}
if slices.Contains(rc.SubResourceIDs, resourceID) {
resourceControl = rc
return nil, stop
for _, subResourceID := range rc.SubResourceIDs {
if subResourceID == resourceID {
resourceControl = rc
return nil, stop
}
}
return &portainer.ResourceControl{}, nil
-7
View File
@@ -31,13 +31,6 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
service: service,
tx: tx,
}
}
// Settings retrieve the ssl settings object.
func (service *Service) Settings() (*portainer.SSLSettings, error) {
var settings portainer.SSLSettings
-31
View File
@@ -1,31 +0,0 @@
package ssl
import (
portainer "github.com/portainer/portainer/api"
)
type ServiceTx struct {
service *Service
tx portainer.Transaction
}
func (service ServiceTx) BucketName() string {
return BucketName
}
// Settings retrieve the settings object.
func (service ServiceTx) Settings() (*portainer.SSLSettings, error) {
var settings portainer.SSLSettings
err := service.tx.GetObject(BucketName, []byte(key), &settings)
if err != nil {
return nil, err
}
return &settings, nil
}
// UpdateSettings persists a Settings object.
func (service ServiceTx) UpdateSettings(settings *portainer.SSLSettings) error {
return service.tx.UpdateObject(BucketName, []byte(key), settings)
}
+10 -13
View File
@@ -4,18 +4,17 @@ import (
"testing"
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/filesystem"
"github.com/google/uuid"
"github.com/gofrs/uuid"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/filesystem"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newGuidString(t *testing.T) string {
uuid, err := uuid.NewRandom()
require.NoError(t, err)
uuid, err := uuid.NewV4()
assert.NoError(t, err)
return uuid.String()
}
@@ -27,11 +26,10 @@ type stackBuilder struct {
}
func TestService_StackByWebhookID(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
_, store := datastore.MustNewTestStore(t, false, true)
_, store := datastore.MustNewTestStore(t, true, true)
b := stackBuilder{t: t, store: store}
b.createNewStack(newGuidString(t))
@@ -43,7 +41,7 @@ func TestService_StackByWebhookID(t *testing.T) {
// can find a stack by webhook ID
got, err := store.StackService.StackByWebhookID(webhookID)
require.NoError(t, err)
assert.NoError(t, err)
assert.Equal(t, stack, *got)
// returns nil and object not found error if there's no stack associated with the webhook
@@ -85,11 +83,10 @@ func (b *stackBuilder) createNewStack(webhookID string) portainer.Stack {
}
func Test_RefreshableStacks(t *testing.T) {
t.Parallel()
if testing.Short() {
t.Skip("skipping test in short mode. Normally takes ~1s to run.")
}
_, store := datastore.MustNewTestStore(t, false, true)
_, store := datastore.MustNewTestStore(t, true, true)
staticStack := portainer.Stack{ID: 1}
stackWithWebhook := portainer.Stack{ID: 2, AutoUpdate: &portainer.AutoUpdateSettings{Webhook: "webhook"}}
@@ -97,10 +94,10 @@ func Test_RefreshableStacks(t *testing.T) {
for _, stack := range []*portainer.Stack{&staticStack, &stackWithWebhook, &refreshableStack} {
err := store.Stack().Create(stack)
require.NoError(t, err)
assert.NoError(t, err)
}
stacks, err := store.Stack().RefreshableStacks()
require.NoError(t, err)
assert.NoError(t, err)
assert.ElementsMatch(t, []portainer.Stack{refreshableStack}, stacks)
}
+6 -29
View File
@@ -3,45 +3,22 @@ package tests
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices/errors"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type teamBuilder struct {
t *testing.T
count int
store *datastore.Store
}
func (b *teamBuilder) createNew(name string) *portainer.Team {
b.count++
team := &portainer.Team{
ID: portainer.TeamID(b.count),
Name: name,
}
err := b.store.Team().Create(team)
assert.NoError(b.t, err)
return team
}
func Test_teamByName(t *testing.T) {
t.Parallel()
t.Run("When store is empty should return ErrObjectNotFound", func(t *testing.T) {
_, store := datastore.MustNewTestStore(t, false, true)
_, store := datastore.MustNewTestStore(t, true, true)
_, err := store.Team().TeamByName("name")
require.ErrorIs(t, err, errors.ErrObjectNotFound)
assert.ErrorIs(t, err, errors.ErrObjectNotFound)
})
t.Run("When there is no object with the same name should return ErrObjectNotFound", func(t *testing.T) {
_, store := datastore.MustNewTestStore(t, false, true)
_, store := datastore.MustNewTestStore(t, true, true)
teamBuilder := teamBuilder{
t: t,
@@ -52,11 +29,11 @@ func Test_teamByName(t *testing.T) {
teamBuilder.createNew("name1")
_, err := store.Team().TeamByName("name")
require.ErrorIs(t, err, errors.ErrObjectNotFound)
assert.ErrorIs(t, err, errors.ErrObjectNotFound)
})
t.Run("When there is an object with the same name should return the object", func(t *testing.T) {
_, store := datastore.MustNewTestStore(t, false, true)
_, store := datastore.MustNewTestStore(t, true, true)
teamBuilder := teamBuilder{
t: t,
@@ -67,7 +44,7 @@ func Test_teamByName(t *testing.T) {
expectedTeam := teamBuilder.createNew("name1")
team, err := store.Team().TeamByName("name1")
require.NoError(t, err, "TeamByName should succeed")
assert.NoError(t, err, "TeamByName should succeed")
assert.Equal(t, expectedTeam, team)
})
}
+28
View File
@@ -0,0 +1,28 @@
package tests
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/stretchr/testify/assert"
)
type teamBuilder struct {
t *testing.T
count int
store *datastore.Store
}
func (b *teamBuilder) createNew(name string) *portainer.Team {
b.count++
team := &portainer.Team{
ID: portainer.TeamID(b.count),
Name: name,
}
err := b.store.Team().Create(team)
assert.NoError(b.t, err)
return team
}
-12
View File
@@ -36,18 +36,6 @@ func (service ServiceTx) UserByUsername(username string) (*portainer.User, error
return nil, err
}
func (service ServiceTx) UserIDByUsername(username string) (portainer.UserID, error) {
user, err := service.UserByUsername(username)
if err != nil {
return 0, err
}
if user == nil {
return 0, dserrors.ErrObjectNotFound
}
return user.ID, nil
}
// UsersByRole return an array containing all the users with the specified role.
func (service ServiceTx) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
-12
View File
@@ -65,18 +65,6 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
return nil, err
}
func (service *Service) UserIDByUsername(username string) (portainer.UserID, error) {
user, err := service.UserByUsername(username)
if err != nil {
return 0, err
}
if user == nil {
return 0, dserrors.ErrObjectNotFound
}
return user.ID, nil
}
// UsersByRole return an array containing all the users with the specified role.
func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User, error) {
var users = make([]portainer.User, 0)
-70
View File
@@ -1,70 +0,0 @@
package version
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/dataservices"
)
type ServiceTx struct {
dataservices.BaseDataServiceTx[models.Version, int] // ID is not used
}
func (tx ServiceTx) InstanceID() (string, error) {
v, err := tx.Version()
if err != nil {
return "", err
}
return v.InstanceID, nil
}
func (tx ServiceTx) UpdateInstanceID(ID string) error {
v, err := tx.Version()
if err != nil {
if !dataservices.IsErrObjectNotFound(err) {
return err
}
v = &models.Version{}
}
v.InstanceID = ID
return tx.UpdateVersion(v)
}
func (tx ServiceTx) Edition() (portainer.SoftwareEdition, error) {
v, err := tx.Version()
if err != nil {
return 0, err
}
return portainer.SoftwareEdition(v.Edition), nil
}
func (tx ServiceTx) Version() (*models.Version, error) {
var v models.Version
err := tx.Tx.GetObject(BucketName, []byte(versionKey), &v)
if err != nil {
return nil, err
}
return &v, nil
}
func (tx ServiceTx) UpdateVersion(version *models.Version) error {
return tx.Tx.UpdateObject(BucketName, []byte(versionKey), version)
}
func (tx ServiceTx) SchemaVersion() (string, error) {
var v models.Version
err := tx.Tx.GetObject(BucketName, []byte(versionKey), &v)
if err != nil {
return "", err
}
return v.SchemaVersion, nil
}
-10
View File
@@ -33,16 +33,6 @@ func NewService(connection portainer.Connection) (*Service, error) {
}, nil
}
func (service *Service) Tx(tx portainer.Transaction) ServiceTx {
return ServiceTx{
BaseDataServiceTx: dataservices.BaseDataServiceTx[models.Version, int]{
Bucket: BucketName,
Connection: service.connection,
Tx: tx,
},
}
}
func (service *Service) SchemaVersion() (string, error) {
v, err := service.Version()
if err != nil {
+20 -30
View File
@@ -14,40 +14,33 @@ import (
// corruption and if a path is not given a default is used.
// The path or an error are returned.
func (store *Store) Backup(path string) (string, error) {
if err := store.Close(); err != nil {
return "", fmt.Errorf("failed to close store before backup: %w", err)
}
filename, err := store.backupDBFile(path)
if err != nil {
return "", err
}
if _, err := store.Open(); err != nil {
return "", fmt.Errorf("failed to reopen store after backup: %w", err)
}
return filename, nil
}
// backupDBFile copies the database file to the backup location.
// Does not manage connection state - works with the database file directly regardless of connection state.
func (store *Store) backupDBFile(backupPath string) (string, error) {
if err := store.createBackupPath(); err != nil {
return "", err
}
backupFilename := store.backupFilename()
if backupPath != "" {
backupFilename = backupPath
if path != "" {
backupFilename = path
}
log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msgf("Backing up database")
// Close the store before backing up
err := store.Close()
if err != nil {
return "", fmt.Errorf("failed to close store before backup: %w", err)
}
log.Info().Str("from", store.connection.GetDatabaseFilePath()).Str("to", backupFilename).Msg("Backing up database")
if err := store.fileService.Copy(store.connection.GetDatabaseFilePath(), backupFilename, true); err != nil {
err = store.fileService.Copy(store.connection.GetDatabaseFilePath(), backupFilename, true)
if err != nil {
return "", fmt.Errorf("failed to create backup file: %w", err)
}
// reopen the store
_, err = store.Open()
if err != nil {
return "", fmt.Errorf("failed to reopen store after backup: %w", err)
}
return backupFilename, nil
}
@@ -57,17 +50,15 @@ func (store *Store) Restore() error {
}
func (store *Store) RestoreFromFile(backupFilename string) error {
if err := store.Close(); err != nil {
return err
}
store.Close()
if err := store.fileService.Copy(backupFilename, store.connection.GetDatabaseFilePath(), true); err != nil {
return fmt.Errorf("unable to restore backup file %q. err: %w", backupFilename, err)
}
log.Info().Str("from", backupFilename).Str("to", store.connection.GetDatabaseFilePath()).Msgf("database restored")
if _, err := store.Open(); err != nil {
_, err := store.Open()
if err != nil {
return fmt.Errorf("unable to determine version of restored portainer backup file: %w", err)
}
@@ -89,7 +80,6 @@ func (store *Store) createBackupPath() error {
return fmt.Errorf("unable to create backup folder: %w", err)
}
}
return nil
}
+9 -75
View File
@@ -1,21 +1,19 @@
package datastore
import (
"os"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/database/models"
"github.com/stretchr/testify/require"
"github.com/rs/zerolog/log"
)
func TestStoreCreation(t *testing.T) {
t.Parallel()
_, store := MustNewTestStore(t, true, true)
require.NotNil(t, store)
if store == nil {
t.Fatal("Expect to create a store")
}
v, err := store.VersionService.Version()
if err != nil {
@@ -32,7 +30,6 @@ func TestStoreCreation(t *testing.T) {
}
func TestBackup(t *testing.T) {
t.Parallel()
_, store := MustNewTestStore(t, true, true)
backupFileName := store.backupFilename()
t.Run("Backup should create "+backupFileName, func(t *testing.T) {
@@ -40,12 +37,8 @@ func TestBackup(t *testing.T) {
Edition: int(portainer.PortainerCE),
SchemaVersion: portainer.APIVersion,
}
err := store.VersionService.UpdateVersion(&v)
require.NoError(t, err)
_, err = store.Backup("")
require.NoError(t, err)
store.VersionService.UpdateVersion(&v)
store.Backup("")
if !isFileExist(backupFileName) {
t.Errorf("Expect backup file to be created %s", backupFileName)
@@ -54,7 +47,6 @@ func TestBackup(t *testing.T) {
}
func TestRestore(t *testing.T) {
t.Parallel()
_, store := MustNewTestStore(t, true, false)
t.Run("Basic Restore", func(t *testing.T) {
@@ -62,14 +54,10 @@ func TestRestore(t *testing.T) {
updateEdition(store, portainer.PortainerCE)
updateVersion(store, "2.4")
_, err := store.Backup("")
require.NoError(t, err)
store.Backup("")
updateVersion(store, "2.16")
testVersion(store, "2.16", t)
err = store.Restore()
require.NoError(t, err)
store.Restore()
// check if the restore is successful and the version is correct
testVersion(store, "2.4", t)
@@ -79,67 +67,13 @@ func TestRestore(t *testing.T) {
// override and set initial db version and edition
updateEdition(store, portainer.PortainerCE)
updateVersion(store, "2.4")
_, err := store.Backup("")
require.NoError(t, err)
store.Backup("")
updateVersion(store, "2.14")
updateVersion(store, "2.16")
testVersion(store, "2.16", t)
err = store.Restore()
require.NoError(t, err)
store.Restore()
// check if the restore is successful and the version is correct
testVersion(store, "2.4", t)
})
}
func TestBackupDBFile(t *testing.T) {
t.Parallel()
_, store := MustNewTestStore(t, true, false)
t.Run("creates backup file without managing connection state", func(t *testing.T) {
// Verify connection is usable before
_, err := store.VersionService.Version()
require.NoError(t, err, "connection should be usable before backupDBFile")
// backupDBFile should work without closing the connection
backupFilename, err := store.backupDBFile("")
require.NoError(t, err)
require.FileExists(t, backupFilename)
// Verify connection is still usable after (not closed/reopened)
_, err = store.VersionService.Version()
require.NoError(t, err, "connection should still be usable after backupDBFile")
require.NoError(t, os.Remove(backupFilename))
})
t.Run("uses custom path when provided", func(t *testing.T) {
customPath := t.TempDir() + "/custom-backup.db"
backupFilename, err := store.backupDBFile(customPath)
require.NoError(t, err)
require.Equal(t, customPath, backupFilename)
require.FileExists(t, backupFilename)
})
}
func TestBackupDBFileUsesCorrectPath(t *testing.T) {
t.Parallel()
_, store := MustNewTestStore(t, true, false)
t.Run("backs up unencrypted db when encrypted flag is false", func(t *testing.T) {
store.connection.SetEncrypted(false)
backupFilename, err := store.backupDBFile("")
require.NoError(t, err)
require.FileExists(t, backupFilename)
// Verify it backed up the unencrypted file (portainer.db)
require.Contains(t, backupFilename, boltdb.DatabaseFileName)
require.NotContains(t, backupFilename, boltdb.EncryptedDatabaseFileName)
require.NoError(t, os.Remove(backupFilename))
})
}
+42 -34
View File
@@ -32,38 +32,34 @@ func (store *Store) Open() (newStore bool, err error) {
}
if encryptionReq {
// NeedsEncryptionMigration() sets encrypted=true as a side effect when a key exists.
// We need to set it back to false so GetDatabaseFilePath() returns the path to the
// actual unencrypted file (portainer.db) that we want to back up.
store.connection.SetEncrypted(false)
// Use backupDBFile directly since connection isn't open yet
// and we don't want to trigger the close/open cycle of Backup()
backupFilename, err := store.backupDBFile("")
backupFilename, err := store.Backup("")
if err != nil {
return false, fmt.Errorf("failed to backup database prior to encrypting: %w", err)
}
if err := store.encryptDB(); err != nil {
innerErr := store.RestoreFromFile(backupFilename) // restore from backup if encryption fails
return false, errors.Join(err, innerErr)
err = store.encryptDB()
if err != nil {
store.RestoreFromFile(backupFilename) // restore from backup if encryption fails
return false, err
}
}
if err := store.connection.Open(); err != nil {
err = store.connection.Open()
if err != nil {
return false, err
}
if err := store.initServices(); err != nil {
err = store.initServices()
if err != nil {
return false, err
}
// If no settings object exists then assume we have a new store
if _, err := store.SettingsService.Settings(); err != nil {
_, err = store.SettingsService.Settings()
if err != nil {
if store.IsErrObjectNotFound(err) {
return true, nil
}
return false, err
}
@@ -76,13 +72,19 @@ func (store *Store) Close() error {
func (store *Store) UpdateTx(fn func(dataservices.DataStoreTx) error) error {
return store.connection.UpdateTx(func(tx portainer.Transaction) error {
return fn(&StoreTx{store: store, tx: tx})
return fn(&StoreTx{
store: store,
tx: tx,
})
})
}
func (store *Store) ViewTx(fn func(dataservices.DataStoreTx) error) error {
return store.connection.ViewTx(func(tx portainer.Transaction) error {
return fn(&StoreTx{store: store, tx: tx})
return fn(&StoreTx{
store: store,
tx: tx,
})
})
}
@@ -97,7 +99,6 @@ func (store *Store) CheckCurrentEdition() error {
if store.edition() != portainer.Edition {
return portainerErrors.ErrWrongDBEdition
}
return nil
}
@@ -106,7 +107,6 @@ func (store *Store) edition() portainer.SoftwareEdition {
if store.IsErrObjectNotFound(err) {
edition = portainer.PortainerCE
}
return edition
}
@@ -125,11 +125,13 @@ func (store *Store) Rollback(force bool) error {
func (store *Store) encryptDB() error {
store.connection.SetEncrypted(false)
if err := store.connection.Open(); err != nil {
err := store.connection.Open()
if err != nil {
return err
}
if err := store.initServices(); err != nil {
err = store.initServices()
if err != nil {
return err
}
@@ -142,7 +144,8 @@ func (store *Store) encryptDB() error {
log.Info().Str("filename", exportFilename).Msg("exporting database backup")
if err := store.Export(exportFilename); err != nil {
err = store.Export(exportFilename)
if err != nil {
log.Error().Str("filename", exportFilename).Err(err).Msg("failed to export")
return err
@@ -151,33 +154,38 @@ func (store *Store) encryptDB() error {
log.Info().Msg("database backup exported")
// Close existing un-encrypted db so that we can delete the file later
if err := store.connection.Close(); err != nil {
store.connection.Close()
// Tell the db layer to create an encrypted db when opened
store.connection.SetEncrypted(true)
store.connection.Open()
// We have to init services before import
err = store.initServices()
if err != nil {
return err
}
if err := store.Import(exportFilename); err != nil {
log.Error().Err(err).Msg("failed to import database backup")
err = store.Import(exportFilename)
if err != nil {
// Remove the new encrypted file that we failed to import
if err := os.Remove(store.connection.GetDatabaseFilePath()); err != nil {
log.Error().Msg("failed to remove the file after import failure")
}
os.Remove(store.connection.GetDatabaseFilePath())
log.Fatal().Err(portainerErrors.ErrDBImportFailed).Msg("")
}
if err := os.Remove(oldFilename); err != nil {
err = os.Remove(oldFilename)
if err != nil {
log.Error().Msg("failed to remove the un-encrypted db file")
}
if err := os.Remove(exportFilename); err != nil {
err = os.Remove(exportFilename)
if err != nil {
log.Error().Msg("failed to remove the json backup file")
}
// Close db connection
if err := store.connection.Close(); err != nil {
return err
}
store.connection.Close()
log.Info().Msg("database successfully encrypted")
+94 -57
View File
@@ -6,14 +6,12 @@ import (
"strings"
"testing"
"github.com/dchest/uniuri"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/crypto"
"github.com/dchest/uniuri"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
@@ -29,36 +27,59 @@ const (
// TestStoreFull an eventually comprehensive set of tests for the Store.
// The idea is what we write to the store, we should read back.
func TestStoreFull(t *testing.T) {
t.Parallel()
_, store := MustNewTestStore(t, true, true)
testCases := map[string]func(t *testing.T){
"User Accounts": store.testUserAccounts,
"Environments": store.testEnvironments,
"Settings": store.testSettings,
"SSL Settings": store.testSSLSettings,
"Tunnel Server": store.testTunnelServer,
"Custom Templates": store.testCustomTemplates,
"Registries": store.testRegistries,
"Resource Control": store.testResourceControl,
"Schedules": store.testSchedules,
"Tags": store.testTags,
"User Accounts": func(t *testing.T) {
store.testUserAccounts(t)
},
"Environments": func(t *testing.T) {
store.testEnvironments(t)
},
"Settings": func(t *testing.T) {
store.testSettings(t)
},
"SSL Settings": func(t *testing.T) {
store.testSSLSettings(t)
},
"Tunnel Server": func(t *testing.T) {
store.testTunnelServer(t)
},
"Custom Templates": func(t *testing.T) {
store.testCustomTemplates(t)
},
"Registries": func(t *testing.T) {
store.testRegistries(t)
},
"Resource Control": func(t *testing.T) {
store.testResourceControl(t)
},
"Schedules": func(t *testing.T) {
store.testSchedules(t)
},
"Tags": func(t *testing.T) {
store.testTags(t)
},
// "Test Title": func(t *testing.T) {
// },
}
for name, test := range testCases {
t.Run(name, test)
}
}
func (store *Store) testEnvironments(t *testing.T) {
id := store.CreateEndpoint(t, "local", portainer.KubernetesLocalEnvironment, "", true)
store.CreateEndpointRelation(t, id)
store.CreateEndpointRelation(id)
id = store.CreateEndpoint(t, "agent", portainer.AgentOnDockerEnvironment, agentOnDockerEnvironmentUrl, true)
store.CreateEndpointRelation(t, id)
store.CreateEndpointRelation(id)
id = store.CreateEndpoint(t, "edge", portainer.EdgeAgentOnKubernetesEnvironment, edgeAgentOnKubernetesEnvironmentUrl, true)
store.CreateEndpointRelation(t, id)
store.CreateEndpointRelation(id)
}
func newEndpoint(endpointType portainer.EndpointType, id portainer.EndpointID, name, URL string, TLS bool) *portainer.Endpoint {
@@ -91,7 +112,18 @@ func newEndpoint(endpointType portainer.EndpointType, id portainer.EndpointID, n
}
func setEndpointAuthorizations(endpoint *portainer.Endpoint) {
endpoint.SecuritySettings = portainer.DefaultEndpointSecuritySettings()
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
AllowVolumeBrowserForRegularUsers: false,
EnableHostManagementFeatures: false,
AllowSysctlSettingForRegularUsers: true,
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
AllowHostNamespaceForRegularUsers: true,
AllowContainerCapabilitiesForRegularUsers: true,
AllowDeviceMappingForRegularUsers: true,
AllowStackManagementForRegularUsers: true,
}
}
func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType portainer.EndpointType, URL string, tls bool) portainer.EndpointID {
@@ -132,25 +164,22 @@ func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType porta
}
setEndpointAuthorizations(expectedEndpoint)
err := store.Endpoint().Create(expectedEndpoint)
require.NoError(t, err)
store.Endpoint().Create(expectedEndpoint)
endpoint, err := store.Endpoint().Endpoint(id)
require.NoError(t, err, "Endpoint() should not return an error")
is.NoError(err, "Endpoint() should not return an error")
is.Equal(expectedEndpoint, endpoint, "endpoint should be the same")
return endpoint.ID
}
func (store *Store) CreateEndpointRelation(t *testing.T, id portainer.EndpointID) {
func (store *Store) CreateEndpointRelation(id portainer.EndpointID) {
relation := &portainer.EndpointRelation{
EndpointID: id,
EdgeStacks: map[portainer.EdgeStackID]bool{},
}
err := store.EndpointRelation().Create(relation)
require.NoError(t, err)
store.EndpointRelation().Create(relation)
}
func (store *Store) testSSLSettings(t *testing.T) {
@@ -162,11 +191,10 @@ func (store *Store) testSSLSettings(t *testing.T) {
SelfSigned: true,
}
err := store.SSLSettings().UpdateSettings(ssl)
require.NoError(t, err)
store.SSLSettings().UpdateSettings(ssl)
settings, err := store.SSLSettings().Settings()
require.NoError(t, err, "Get sslsettings should succeed")
is.NoError(err, "Get sslsettings should succeed")
is.Equal(ssl, settings, "Stored SSLSettings should be the same as what is read out")
}
@@ -175,27 +203,27 @@ func (store *Store) testTunnelServer(t *testing.T) {
expectPrivateKeySeed := uniuri.NewLen(16)
err := store.TunnelServer().UpdateInfo(&portainer.TunnelServerInfo{PrivateKeySeed: expectPrivateKeySeed})
require.NoError(t, err, "UpdateInfo should have succeeded")
is.NoError(err, "UpdateInfo should have succeeded")
serverInfo, err := store.TunnelServer().Info()
require.NoError(t, err, "Info should have succeeded")
is.NoError(err, "Info should have succeeded")
is.Equal(expectPrivateKeySeed, serverInfo.PrivateKeySeed, "hashed passwords should not differ")
}
// add users, read them back and check the details are unchanged
func (store *Store) testUserAccounts(t *testing.T) {
err := store.createAccount(adminUsername, adminPassword, portainer.AdministratorRole)
require.NoError(t, err, "CreateAccount should succeed")
is := assert.New(t)
err = store.checkAccount(adminUsername, adminPassword, portainer.AdministratorRole)
require.NoError(t, err, "Account failure")
err := store.createAccount(adminUsername, adminPassword, portainer.AdministratorRole)
is.NoError(err, "CreateAccount should succeed")
store.checkAccount(adminUsername, adminPassword, portainer.AdministratorRole)
is.NoError(err, "Account failure")
err = store.createAccount(standardUsername, standardPassword, portainer.StandardUserRole)
require.NoError(t, err, "CreateAccount should succeed")
err = store.checkAccount(standardUsername, standardPassword, portainer.StandardUserRole)
require.NoError(t, err, "Account failure")
is.NoError(err, "CreateAccount should succeed")
store.checkAccount(standardUsername, standardPassword, portainer.StandardUserRole)
is.NoError(err, "Account failure")
}
// create an account with the provided details
@@ -210,7 +238,12 @@ func (store *Store) createAccount(username, password string, role portainer.User
return err
}
return store.User().Create(user)
err = store.User().Create(user)
if err != nil {
return err
}
return nil
}
func (store *Store) checkAccount(username, expectPassword string, expectRole portainer.UserRole) error {
@@ -227,7 +260,12 @@ func (store *Store) checkAccount(username, expectPassword string, expectRole por
// Check the password
cs := crypto.Service{}
if cs.CompareHashAndData(user.Password, expectPassword) != nil {
expectPasswordHash, err := cs.Hash(expectPassword)
if err != nil {
return errors.Wrap(err, "hash failed")
}
if user.Password != expectPasswordHash {
return fmt.Errorf("%s user password hash failure", user.Username)
}
@@ -239,7 +277,7 @@ func (store *Store) testSettings(t *testing.T) {
// since many settings are default and basically nil, I'm going to update some and read them back
expectedSettings, err := store.Settings().Settings()
require.NoError(t, err, "Settings() should not return an error")
is.NoError(err, "Settings() should not return an error")
expectedSettings.TemplatesURL = "http://portainer.io/application-templates"
expectedSettings.HelmRepositoryURL = "http://portainer.io/helm-repository"
expectedSettings.EdgeAgentCheckinInterval = 60
@@ -253,10 +291,10 @@ func (store *Store) testSettings(t *testing.T) {
expectedSettings.SnapshotInterval = "10m"
err = store.Settings().UpdateSettings(expectedSettings)
require.NoError(t, err, "UpdateSettings() should succeed")
is.NoError(err, "UpdateSettings() should succeed")
settings, err := store.Settings().Settings()
require.NoError(t, err, "Settings() should not return an error")
is.NoError(err, "Settings() should not return an error")
is.Equal(expectedSettings, settings, "stored settings should match")
}
@@ -276,11 +314,10 @@ func (store *Store) testCustomTemplates(t *testing.T) {
CreatedByUserID: 10,
}
err := customTemplate.Create(expectedTemplate)
require.NoError(t, err)
customTemplate.Create(expectedTemplate)
actualTemplate, err := customTemplate.Read(expectedTemplate.ID)
require.NoError(t, err, "CustomTemplate should not return an error")
is.NoError(err, "CustomTemplate should not return an error")
is.Equal(expectedTemplate, actualTemplate, "expected and actual template do not match")
}
@@ -308,17 +345,17 @@ func (store *Store) testRegistries(t *testing.T) {
}
err := regService.Create(reg1)
require.NoError(t, err)
is.NoError(err)
err = regService.Create(reg2)
require.NoError(t, err)
is.NoError(err)
actualReg1, err := regService.Read(reg1.ID)
require.NoError(t, err)
is.NoError(err)
is.Equal(reg1, actualReg1, "registries differ")
actualReg2, err := regService.Read(reg2.ID)
require.NoError(t, err)
is.NoError(err)
is.Equal(reg2, actualReg2, "registries differ")
}
@@ -341,10 +378,10 @@ func (store *Store) testSchedules(t *testing.T) {
}
err := schedule.CreateSchedule(s)
require.NoError(t, err, "CreateSchedule should succeed")
is.NoError(err, "CreateSchedule should succeed")
actual, err := schedule.Schedule(s.ID)
require.NoError(t, err, "schedule should be found")
is.NoError(err, "schedule should be found")
is.Equal(s, actual, "schedules differ")
}
@@ -364,16 +401,16 @@ func (store *Store) testTags(t *testing.T) {
}
err := tags.Create(tag1)
require.NoError(t, err, "Tags.Create should succeed")
is.NoError(err, "Tags.Create should succeed")
err = tags.Create(tag2)
require.NoError(t, err, "Tags.Create should succeed")
is.NoError(err, "Tags.Create should succeed")
actual, err := tags.Read(tag1.ID)
require.NoError(t, err, "tag1 should be found")
is.NoError(err, "tag1 should be found")
is.Equal(tag1, actual, "tags differ")
actual, err = tags.Read(tag2.ID)
require.NoError(t, err, "tag2 should be found")
is.NoError(err, "tag2 should be found")
is.Equal(tag2, actual, "tags differ")
}
+2
View File
@@ -31,6 +31,7 @@ func (store *Store) checkOrCreateDefaultSettings() error {
settings, err := store.SettingsService.Settings()
if store.IsErrObjectNotFound(err) {
defaultSettings := &portainer.Settings{
EnableTelemetry: false,
AuthenticationMethod: portainer.AuthenticationInternal,
BlackListedLabels: make([]portainer.Pair, 0),
InternalAuthSettings: portainer.InternalAuthSettings{
@@ -59,6 +60,7 @@ func (store *Store) checkOrCreateDefaultSettings() error {
KubectlShellImage: *store.flags.KubectlShellImage,
IsDockerDesktopExtension: isDDExtention,
EnforceEdgeID: true,
}
return store.SettingsService.UpdateSettings(defaultSettings)
+36 -64
View File
@@ -6,18 +6,17 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"testing"
"github.com/Masterminds/semver"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/portainer/portainer/api/filesystem"
"github.com/Masterminds/semver/v3"
"github.com/google/go-cmp/cmp"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
)
func TestMigrateData(t *testing.T) {
@@ -54,11 +53,9 @@ func TestMigrateData(t *testing.T) {
}
testVersion(store, portainer.APIVersion, t)
err := store.Close()
require.NoError(t, err)
store.Close()
newStore, err = store.Open()
require.NoError(t, err)
newStore, _ = store.Open()
if newStore {
t.Error("Expect store to NOT be new DB")
}
@@ -66,11 +63,8 @@ func TestMigrateData(t *testing.T) {
t.Run("MigrateData should create backup file upon update", func(t *testing.T) {
_, store := MustNewTestStore(t, true, false)
err := store.VersionService.UpdateVersion(&models.Version{SchemaVersion: "2.0", Edition: int(portainer.PortainerCE)})
require.NoError(t, err)
err = store.MigrateData()
require.NoError(t, err)
store.VersionService.UpdateVersion(&models.Version{SchemaVersion: "1.0", Edition: int(portainer.PortainerCE)})
store.MigrateData()
backupfilename := store.backupFilename()
if exists, _ := store.fileService.FileExists(backupfilename); !exists {
@@ -79,28 +73,21 @@ func TestMigrateData(t *testing.T) {
})
t.Run("MigrateData should recover and restore backup during migration critical failure", func(t *testing.T) {
t.Setenv("PORTAINER_TEST_MIGRATE_FAIL", "FAIL")
os.Setenv("PORTAINER_TEST_MIGRATE_FAIL", "FAIL")
version := "2.15"
_, store := MustNewTestStore(t, true, false)
store.VersionService.UpdateVersion(&models.Version{SchemaVersion: version, Edition: int(portainer.PortainerCE)})
store.MigrateData()
err := store.VersionService.UpdateVersion(&models.Version{SchemaVersion: version, Edition: int(portainer.PortainerCE)})
require.NoError(t, err)
err = store.MigrateData()
require.Error(t, err)
store.Open()
testVersion(store, version, t)
})
t.Run("MigrateData should fail to create backup if database file is set to updating", func(t *testing.T) {
_, store := MustNewTestStore(t, true, false)
err := store.VersionService.StoreIsUpdating(true)
require.NoError(t, err)
err = store.MigrateData()
require.Error(t, err)
store.VersionService.StoreIsUpdating(true)
store.MigrateData()
// If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected!
// If the backup file is not blank, then it means a backup was created. We don't want that because we
@@ -128,12 +115,10 @@ func TestMigrateData(t *testing.T) {
if latestMigrations.Version.Equal(semver.MustParse(portainer.APIVersion)) {
v.MigratorCount = len(latestMigrations.MigrationFuncs)
err = store.VersionService.UpdateVersion(v)
require.NoError(t, err)
store.VersionService.UpdateVersion(v)
}
err = store.MigrateData()
require.NoError(t, err)
store.MigrateData()
// If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected!
// If the backup file is not blank, then it means a backup was created. We don't want that because we
@@ -156,12 +141,8 @@ func TestMigrateData(t *testing.T) {
}
v.MigratorCount = 1000
err = store.VersionService.UpdateVersion(v)
require.NoError(t, err)
err = store.MigrateData()
require.NoError(t, err)
store.VersionService.UpdateVersion(v)
store.MigrateData()
// If you get an error, it usually means that the backup folder doesn't exist (no backups). Expected!
// If the backup file is not blank, then it means a backup was created. We don't want that because we
@@ -174,18 +155,17 @@ func TestMigrateData(t *testing.T) {
}
func TestRollback(t *testing.T) {
t.Parallel()
t.Run("Rollback should restore upgrade after backup", func(t *testing.T) {
version := "2.11"
v := models.Version{SchemaVersion: version}
v := models.Version{
SchemaVersion: version,
}
_, store := MustNewTestStore(t, false, false)
store.VersionService.UpdateVersion(&v)
err := store.VersionService.UpdateVersion(&v)
require.NoError(t, err)
_, err = store.Backup("")
_, err := store.Backup("")
if err != nil {
log.Fatal().Err(err).Msg("")
}
@@ -204,9 +184,7 @@ func TestRollback(t *testing.T) {
return
}
_, err = store.Open()
require.NoError(t, err)
store.Open()
testVersion(store, version, t)
})
@@ -219,11 +197,9 @@ func TestRollback(t *testing.T) {
}
_, store := MustNewTestStore(t, true, false)
store.VersionService.UpdateVersion(&v)
err := store.VersionService.UpdateVersion(&v)
require.NoError(t, err)
_, err = store.Backup("")
_, err := store.Backup("")
if err != nil {
log.Fatal().Err(err).Msg("")
}
@@ -242,8 +218,7 @@ func TestRollback(t *testing.T) {
return
}
_, err = store.Open()
require.NoError(t, err)
store.Open()
testVersion(store, version, t)
})
}
@@ -262,17 +237,17 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanc
_, store := MustNewTestStore(t, true, false)
fmt.Println("store.path=", store.GetConnection().GetDatabaseFilePath())
err = store.connection.DeleteObject("version", []byte("VERSION"))
require.NoError(t, err)
store.connection.DeleteObject("version", []byte("VERSION"))
// defer teardown()
if err := importJSON(t, bytes.NewReader(srcJSON), store); err != nil {
err = importJSON(t, bytes.NewReader(srcJSON), store)
if err != nil {
return err
}
// Run the actual migrations on our input database.
if err := store.MigrateData(); err != nil {
err = store.MigrateData()
if err != nil {
return err
}
@@ -285,7 +260,8 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanc
}
v.InstanceID = "463d5c47-0ea5-4aca-85b1-405ceefee254"
if err := store.VersionService.UpdateVersion(v); err != nil {
err = store.VersionService.UpdateVersion(v)
if err != nil {
return err
}
}
@@ -294,10 +270,10 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanc
// exportJson rather than ExportRaw. The exportJson function allows us to
// strip out the metadata which we don't want for our tests.
// TODO: update connection interface in CE to allow us to use ExportRaw and pass meta false
if err := store.connection.Close(); err != nil {
err = store.connection.Close()
if err != nil {
t.Fatalf("err closing bolt connection: %v", err)
}
con, ok := store.connection.(*boltdb.DbConnection)
if !ok {
t.Fatalf("backing database is not using boltdb, but the migrations test requires it")
@@ -325,16 +301,12 @@ func migrateDBTestHelper(t *testing.T, srcPath, wantPath string, overrideInstanc
// Compare the result we got with the one we wanted.
if diff := cmp.Diff(wantJSON, gotJSON); diff != "" {
gotPath := filesystem.JoinPaths(os.TempDir(), "portainer-migrator-test-fail.json")
err = os.WriteFile(
gotPath := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
os.WriteFile(
gotPath,
gotJSON,
0o600,
)
if err != nil {
log.Warn().Err(err).Msg("failed writing migrated output to temp file")
}
t.Errorf(
"migrate data from %s to %s failed\nwrote migrated input to %s\nmismatch (-want +got):\n%s",
srcPath,
@@ -26,7 +26,6 @@ func setup(store *Store) error {
}
func TestMigrateSettings(t *testing.T) {
t.Parallel()
_, store := MustNewTestStore(t, false, true)
err := setup(store)
+7 -10
View File
@@ -6,13 +6,10 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore/migrator"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMigrateStackEntryPoint(t *testing.T) {
t.Parallel()
_, store := MustNewTestStore(t, false, true)
stackService := store.Stack()
@@ -31,25 +28,25 @@ func TestMigrateStackEntryPoint(t *testing.T) {
for _, s := range stacks {
err := stackService.Create(s)
require.NoError(t, err, "failed to create stack")
assert.NoError(t, err, "failed to create stack")
}
s, err := stackService.Read(1)
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, s.GitConfig, "first stack should not have git config")
s, err = stackService.Read(2)
require.NoError(t, err)
assert.Empty(t, s.GitConfig.ConfigFilePath, "not migrated yet migrated")
assert.NoError(t, err)
assert.Equal(t, "", s.GitConfig.ConfigFilePath, "not migrated yet migrated")
err = migrator.MigrateStackEntryPoint(stackService)
require.NoError(t, err, "failed to migrate entry point to Git ConfigFilePath")
assert.NoError(t, err, "failed to migrate entry point to Git ConfigFilePath")
s, err = stackService.Read(1)
require.NoError(t, err)
assert.NoError(t, err)
assert.Nil(t, s.GitConfig, "first stack should not have git config")
s, err = stackService.Read(2)
require.NoError(t, err)
assert.NoError(t, err)
assert.Equal(t, "dir/sub/compose.yml", s.GitConfig.ConfigFilePath, "second stack should have config file path migrated")
}
+5 -11
View File
@@ -105,18 +105,12 @@ func (store *Store) getOrMigrateLegacyVersion() (*models.Version, error) {
// finishMigrateLegacyVersion writes the new version to the DB and removes the old version keys from the DB
func (store *Store) finishMigrateLegacyVersion(versionToWrite *models.Version) error {
if err := store.VersionService.UpdateVersion(versionToWrite); err != nil {
return err
}
err := store.VersionService.UpdateVersion(versionToWrite)
// Remove legacy keys if present
if err := store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey)); err != nil {
return err
}
store.connection.DeleteObject(bucketName, []byte(legacyDBVersionKey))
store.connection.DeleteObject(bucketName, []byte(legacyEditionKey))
store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
if err := store.connection.DeleteObject(bucketName, []byte(legacyEditionKey)); err != nil {
return err
}
return store.connection.DeleteObject(bucketName, []byte(legacyInstanceKey))
return err
}
+3 -5
View File
@@ -6,18 +6,16 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/dataservices/edgegroup"
"github.com/portainer/portainer/api/logs"
"github.com/stretchr/testify/require"
)
func TestMigrateEdgeGroupEndpointsToRoars_2_33_0Idempotency(t *testing.T) {
t.Parallel()
var conn portainer.Connection = &boltdb.DbConnection{Path: t.TempDir()}
err := conn.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn)
defer conn.Close()
edgeGroupService, err := edgegroup.NewService(conn)
require.NoError(t, err)
@@ -41,7 +39,7 @@ func TestMigrateEdgeGroupEndpointsToRoars_2_33_0Idempotency(t *testing.T) {
migratedEdgeGroup, err := edgeGroupService.Read(edgeGroup.ID)
require.NoError(t, err)
require.Empty(t, migratedEdgeGroup.Endpoints)
require.Len(t, migratedEdgeGroup.Endpoints, 0)
require.Equal(t, len(edgeGroup.Endpoints), migratedEdgeGroup.EndpointIDs.Len())
// Run migration again to ensure the results didn't change
@@ -52,6 +50,6 @@ func TestMigrateEdgeGroupEndpointsToRoars_2_33_0Idempotency(t *testing.T) {
migratedEdgeGroup, err = edgeGroupService.Read(edgeGroup.ID)
require.NoError(t, err)
require.Empty(t, migratedEdgeGroup.Endpoints)
require.Len(t, migratedEdgeGroup.Endpoints, 0)
require.Equal(t, len(edgeGroup.Endpoints), migratedEdgeGroup.EndpointIDs.Len())
}
-206
View File
@@ -1,206 +0,0 @@
package migrator
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/dataservices/endpoint"
"github.com/portainer/portainer/api/dataservices/pendingactions"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/logs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMigrateRegistryAccessSASecrets_2_40_0(t *testing.T) {
t.Parallel()
var conn portainer.Connection = &boltdb.DbConnection{Path: t.TempDir()}
err := conn.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn)
registryService, err := registry.NewService(conn)
require.NoError(t, err)
endpointService, err := endpoint.NewService(conn)
require.NoError(t, err)
pendingActionsService, err := pendingactions.NewService(conn)
require.NoError(t, err)
t.Run("sets MigrateRegistrySASecrets flag for k8s endpoints with registry access", func(t *testing.T) {
k8sEndpoint := &portainer.Endpoint{
ID: 1,
Name: "k8s-cluster",
Type: portainer.AgentOnKubernetesEnvironment,
}
dockerEndpoint := &portainer.Endpoint{
ID: 2,
Name: "docker-standalone",
Type: portainer.DockerEnvironment,
}
err := conn.CreateObjectWithId(endpoint.BucketName, int(k8sEndpoint.ID), k8sEndpoint)
require.NoError(t, err)
err = conn.CreateObjectWithId(endpoint.BucketName, int(dockerEndpoint.ID), dockerEndpoint)
require.NoError(t, err)
reg := &portainer.Registry{
ID: 1,
Name: "test-registry",
RegistryAccesses: portainer.RegistryAccesses{
k8sEndpoint.ID: portainer.RegistryAccessPolicies{
Namespaces: []string{"default", "production"},
},
dockerEndpoint.ID: portainer.RegistryAccessPolicies{
Namespaces: []string{"ignored"},
},
},
}
err = conn.CreateObjectWithId(registry.BucketName, int(reg.ID), reg)
require.NoError(t, err)
m := NewMigrator(&MigratorParameters{
RegistryService: registryService,
EndpointService: endpointService,
PendingActionsService: pendingActionsService,
})
err = m.migrateRegistryAccessSASecrets_2_40_0()
require.NoError(t, err)
updatedK8sEndpoint, err := endpointService.Endpoint(k8sEndpoint.ID)
require.NoError(t, err)
assert.True(t, updatedK8sEndpoint.PostInitMigrations.MigrateRegistrySASecrets, "should have set MigrateRegistrySASecrets flag for k8s endpoint")
updatedDockerEndpoint, err := endpointService.Endpoint(dockerEndpoint.ID)
require.NoError(t, err)
assert.False(t, updatedDockerEndpoint.PostInitMigrations.MigrateRegistrySASecrets, "should not have set MigrateRegistrySASecrets flag for docker endpoint")
})
t.Run("skips endpoints with empty namespaces", func(t *testing.T) {
conn2 := &boltdb.DbConnection{Path: t.TempDir()}
err := conn2.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn2)
registryService2, _ := registry.NewService(conn2)
endpointService2, _ := endpoint.NewService(conn2)
pendingActionsService2, _ := pendingactions.NewService(conn2)
k8sEndpoint := &portainer.Endpoint{
ID: 10,
Name: "k8s-cluster",
Type: portainer.AgentOnKubernetesEnvironment,
}
err = conn2.CreateObjectWithId(endpoint.BucketName, int(k8sEndpoint.ID), k8sEndpoint)
require.NoError(t, err)
reg := &portainer.Registry{
ID: 10,
Name: "empty-registry",
RegistryAccesses: portainer.RegistryAccesses{
k8sEndpoint.ID: portainer.RegistryAccessPolicies{
Namespaces: []string{},
},
},
}
err = conn2.CreateObjectWithId(registry.BucketName, int(reg.ID), reg)
require.NoError(t, err)
m := NewMigrator(&MigratorParameters{
RegistryService: registryService2,
EndpointService: endpointService2,
PendingActionsService: pendingActionsService2,
})
err = m.migrateRegistryAccessSASecrets_2_40_0()
require.NoError(t, err)
allPAs, err := pendingActionsService2.ReadAll()
require.NoError(t, err)
assert.Empty(t, allPAs, "should not create pending actions for empty namespaces")
})
t.Run("skips non-existent endpoints", func(t *testing.T) {
conn3 := &boltdb.DbConnection{Path: t.TempDir()}
err := conn3.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn3)
registryService3, _ := registry.NewService(conn3)
endpointService3, _ := endpoint.NewService(conn3)
pendingActionsService3, _ := pendingactions.NewService(conn3)
reg := &portainer.Registry{
ID: 20,
Name: "orphan-registry",
RegistryAccesses: portainer.RegistryAccesses{
999: portainer.RegistryAccessPolicies{
Namespaces: []string{"default"},
},
},
}
err = conn3.CreateObjectWithId(registry.BucketName, int(reg.ID), reg)
require.NoError(t, err)
m := NewMigrator(&MigratorParameters{
RegistryService: registryService3,
EndpointService: endpointService3,
PendingActionsService: pendingActionsService3,
})
err = m.migrateRegistryAccessSASecrets_2_40_0()
require.NoError(t, err)
allPAs, err := pendingActionsService3.ReadAll()
require.NoError(t, err)
assert.Empty(t, allPAs, "should not create pending actions for non-existent endpoints")
})
t.Run("idempotent - running twice creates duplicate actions but doesn't error", func(t *testing.T) {
conn4 := &boltdb.DbConnection{Path: t.TempDir()}
err := conn4.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn4)
registryService4, _ := registry.NewService(conn4)
endpointService4, _ := endpoint.NewService(conn4)
pendingActionsService4, _ := pendingactions.NewService(conn4)
k8sEndpoint := &portainer.Endpoint{
ID: 30,
Name: "k8s-cluster",
Type: portainer.AgentOnKubernetesEnvironment,
}
err = conn4.CreateObjectWithId(endpoint.BucketName, int(k8sEndpoint.ID), k8sEndpoint)
require.NoError(t, err)
reg := &portainer.Registry{
ID: 30,
Name: "test-registry",
RegistryAccesses: portainer.RegistryAccesses{
k8sEndpoint.ID: portainer.RegistryAccessPolicies{
Namespaces: []string{"default"},
},
},
}
err = conn4.CreateObjectWithId(registry.BucketName, int(reg.ID), reg)
require.NoError(t, err)
m := NewMigrator(&MigratorParameters{
RegistryService: registryService4,
EndpointService: endpointService4,
PendingActionsService: pendingActionsService4,
})
err = m.migrateRegistryAccessSASecrets_2_40_0()
require.NoError(t, err)
err = m.migrateRegistryAccessSASecrets_2_40_0()
require.NoError(t, err)
})
}
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/Masterminds/semver/v3"
"github.com/Masterminds/semver"
"github.com/rs/zerolog/log"
)
@@ -95,7 +95,7 @@ func (m *Migrator) NeedsMigration() bool {
// In this particular instance we should log a fatal error
if m.CurrentDBEdition() != portainer.PortainerCE {
log.Fatal().Msg("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://docs.portainer.io/faqs/upgrading/can-i-downgrade-from-portainer-business-to-portainer-ce")
log.Fatal().Msg("the Portainer database is set for Portainer Business Edition, please follow the instructions in our documentation to downgrade it: https://documentation.portainer.io/v2.0-be/downgrade/be-to-ce/")
return false
}
@@ -1,58 +0,0 @@
package migrator
import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/rs/zerolog/log"
)
// migrateRegistryAccessSASecrets_2_40_0 marks Kubernetes endpoints that have
// registry access configured so that imagePullSecrets can be added to their
// default ServiceAccounts during the post-init migration phase (when cluster
// access is available).
func (m *Migrator) migrateRegistryAccessSASecrets_2_40_0() error {
log.Info().Msg("migrating registry access service account secrets")
registries, err := m.registryService.ReadAll()
if err != nil {
return err
}
endpoints, err := m.endpointService.Endpoints()
if err != nil {
return err
}
// Collect the IDs of endpoints that have at least one registry with
// non-empty namespace access - these need the SA imagePullSecrets migration.
needsMigration := make(map[portainer.EndpointID]bool)
for _, registry := range registries {
for endpointID, access := range registry.RegistryAccesses {
if len(access.Namespaces) > 0 {
needsMigration[endpointID] = true
}
}
}
for i := range endpoints {
endpoint := &endpoints[i]
if !endpointutils.IsKubernetesEndpoint(endpoint) {
continue
}
if !needsMigration[endpoint.ID] {
continue
}
endpoint.PostInitMigrations.MigrateRegistrySASecrets = true
if err := m.endpointService.UpdateEndpoint(endpoint.ID, endpoint); err != nil {
log.Warn().
Err(err).
Int("endpointID", int(endpoint.ID)).
Msg("failed to set registry SA secret migration flag for endpoint")
}
}
return nil
}
@@ -21,6 +21,7 @@ func (m *Migrator) updateSettingsToDB25() error {
}
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
legacySettings.EnableTelemetry = true
legacySettings.AllowContainerCapabilitiesForRegularUsers = true
+10 -10
View File
@@ -77,12 +77,8 @@ func (m *Migrator) updateRegistriesToDB32() error {
Namespaces: []string{},
}
}
if err := m.registryService.Update(registry.ID, &registry); err != nil {
return err
}
m.registryService.Update(registry.ID, &registry)
}
return nil
}
@@ -125,11 +121,10 @@ func (m *Migrator) updateDockerhubToDB32() error {
if !migrated {
// keep this one entry
migrated = true
} else {
// delete subsequent duplicates
} else if err := m.registryService.Delete(r.ID); err != nil {
return err
m.registryService.Delete(r.ID)
}
}
}
@@ -143,6 +138,7 @@ func (m *Migrator) updateDockerhubToDB32() error {
}
for _, endpoint := range endpoints {
if endpoint.Type != portainer.KubernetesLocalEnvironment &&
endpoint.Type != portainer.AgentOnKubernetesEnvironment &&
endpoint.Type != portainer.EdgeAgentOnKubernetesEnvironment {
@@ -150,14 +146,18 @@ func (m *Migrator) updateDockerhubToDB32() error {
userAccessPolicies := portainer.UserAccessPolicies{}
for userId := range endpoint.UserAccessPolicies {
if _, found := endpoint.UserAccessPolicies[userId]; found {
userAccessPolicies[userId] = portainer.AccessPolicy{RoleID: 0}
userAccessPolicies[userId] = portainer.AccessPolicy{
RoleID: 0,
}
}
}
teamAccessPolicies := portainer.TeamAccessPolicies{}
for teamId := range endpoint.TeamAccessPolicies {
if _, found := endpoint.TeamAccessPolicies[teamId]; found {
teamAccessPolicies[teamId] = portainer.AccessPolicy{RoleID: 0}
teamAccessPolicies[teamId] = portainer.AccessPolicy{
RoleID: 0,
}
}
}
+1 -5
View File
@@ -29,7 +29,7 @@ import (
"github.com/portainer/portainer/api/dataservices/version"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/Masterminds/semver/v3"
"github.com/Masterminds/semver"
"github.com/rs/zerolog/log"
)
@@ -258,10 +258,6 @@ func (m *Migrator) initMigrations() {
m.addMigrations("2.33.1", m.migrateEdgeGroupEndpointsToRoars_2_33_0)
m.addMigrations("2.40.0", m.migrateRegistryAccessSASecrets_2_40_0)
// WARNING: do not change migrations that have already been released!
// Add new migrations above...
// One function per migration, each versions migration funcs in the same file.
}
+2 -8
View File
@@ -6,7 +6,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/api/pendingactions/handlers"
"github.com/stretchr/testify/require"
)
type cleanNAPWithOverridePolicies struct {
@@ -14,14 +13,10 @@ type cleanNAPWithOverridePolicies struct {
}
func Test_ConvertCleanNAPWithOverridePoliciesPayload(t *testing.T) {
t.Parallel()
t.Run("test ConvertCleanNAPWithOverridePoliciesPayload", func(t *testing.T) {
_, store := MustNewTestStore(t, true, false)
defer func() {
err := store.Close()
require.NoError(t, err)
}()
defer store.Close()
gid := portainer.EndpointGroupID(1)
@@ -97,8 +92,7 @@ func Test_ConvertCleanNAPWithOverridePoliciesPayload(t *testing.T) {
})
}
err = store.PendingActions().Delete(d.PendingAction.ID)
require.NoError(t, err)
store.PendingActions().Delete(d.PendingAction.ID)
}
})
}
+55 -181
View File
@@ -1,10 +1,8 @@
package postinit
import (
"cmp"
"context"
"fmt"
"slices"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
@@ -12,9 +10,7 @@ import (
"github.com/portainer/portainer/api/dataservices"
dockerClient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/portainer/portainer/api/logs"
"github.com/portainer/portainer/api/pendingactions/actions"
"github.com/portainer/portainer/pkg/endpoints"
@@ -47,65 +43,40 @@ func NewPostInitMigrator(
// PostInitMigrate will run all post-init migrations, which require docker/kube clients for all edge or non-edge environments
func (postInitMigrator *PostInitMigrator) PostInitMigrate() error {
var environments []portainer.Endpoint
environments, err := postInitMigrator.dataStore.Endpoint().Endpoints()
if err != nil {
log.Error().Err(err).Msg("Error getting environments")
return err
}
if err := postInitMigrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
var err error
if environments, err = tx.Endpoint().ReadAll(func(endpoint portainer.Endpoint) bool {
return endpoints.HasDirectConnectivity(&endpoint)
}); err != nil {
return fmt.Errorf("failed to retrieve environments: %w", err)
}
var pendingActions []portainer.PendingAction
if pendingActions, err = tx.PendingActions().ReadAll(func(action portainer.PendingAction) bool {
return action.Action == actions.PostInitMigrateEnvironment
}); err != nil {
return fmt.Errorf("failed to retrieve pending actions: %w", err)
}
// Sort for the binary search in createPostInitMigrationPendingAction()
slices.SortFunc(pendingActions, func(a, b portainer.PendingAction) int {
return cmp.Compare(a.EndpointID, b.EndpointID)
})
for _, environment := range environments {
if !endpoints.IsEdgeEndpoint(&environment) {
for _, environment := range environments {
// edge environments will run after the server starts, in pending actions
if endpoints.IsEdgeEndpoint(&environment) {
// Skip edge environments that do not have direct connectivity
if !endpoints.HasDirectConnectivity(&environment) {
continue
}
// Edge environments will run after the server starts, in pending actions
log.Info().
Int("endpoint_id", int(environment.ID)).
Msg("adding pending action 'PostInitMigrateEnvironment' for environment")
if err := postInitMigrator.createPostInitMigrationPendingAction(tx, environment.ID, pendingActions); err != nil {
if err := postInitMigrator.createPostInitMigrationPendingAction(environment.ID); err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error creating pending action for environment")
}
} else {
// Non-edge environments will run before the server starts.
if err := postInitMigrator.MigrateEnvironment(&environment); err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error running post-init migrations for non-edge environment")
}
}
return err
}); err != nil {
log.Error().Err(err).Msg("error running post-init migrations")
return err
}
for _, environment := range environments {
if endpoints.IsEdgeEndpoint(&environment) {
continue
}
// Non-edge environments will run before the server starts.
if err := postInitMigrator.MigrateEnvironment(&environment); err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error running post-init migrations for non-edge environment")
}
}
return nil
@@ -113,153 +84,73 @@ func (postInitMigrator *PostInitMigrator) PostInitMigrate() error {
// try to create a post init migration pending action. If it already exists, do nothing
// this function exists for readability, not reusability
// pending actions must be passed in ascending order by endpoint ID
func (postInitMigrator *PostInitMigrator) createPostInitMigrationPendingAction(tx dataservices.DataStoreTx, environmentID portainer.EndpointID, pendingActions []portainer.PendingAction) error {
func (postInitMigrator *PostInitMigrator) createPostInitMigrationPendingAction(environmentID portainer.EndpointID) error {
action := portainer.PendingAction{
EndpointID: environmentID,
Action: actions.PostInitMigrateEnvironment,
}
if _, found := slices.BinarySearchFunc(pendingActions, environmentID, func(e portainer.PendingAction, id portainer.EndpointID) int {
return cmp.Compare(e.EndpointID, id)
}); found {
log.Debug().
Str("action", action.Action).
Int("endpoint_id", int(action.EndpointID)).
Msg("pending action already exists for environment, skipping...")
return nil
pendingActions, err := postInitMigrator.dataStore.PendingActions().ReadAll()
if err != nil {
return fmt.Errorf("failed to retrieve pending actions: %w", err)
}
return tx.PendingActions().Create(&action)
for _, dba := range pendingActions {
if dba.EndpointID == action.EndpointID && dba.Action == action.Action {
log.Debug().
Str("action", action.Action).
Int("endpoint_id", int(action.EndpointID)).
Msg("pending action already exists for environment, skipping...")
return nil
}
}
return postInitMigrator.dataStore.PendingActions().Create(&action)
}
// MigrateEnvironment runs migrations on a single environment
func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endpoint) error {
log.Info().
Int("endpoint_id", int(environment.ID)).
Msg("executing post init migration for environment")
log.Info().Msgf("Executing post init migration for environment %d", environment.ID)
switch {
case endpointutils.IsKubernetesEndpoint(environment):
// get the kubeclient for the environment, and skip all kube migrations if there's an error
kubeclient, err := migrator.kubeFactory.GetPrivilegedKubeClient(environment)
if err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error creating kubeclient for environment")
log.Error().Err(err).Msgf("Error creating kubeclient for environment: %d", environment.ID)
return err
}
// If one environment fails, it is logged and the next migration runs. The error is returned at the end and handled by pending actions
var latestErr error
kubernetesMigrations := []func() error{
func() error { return migrator.MigrateIngresses(*environment, kubeclient) },
func() error { return migrator.MigrateRegistrySASecrets(*environment, kubeclient) },
// if one environment fails, it is logged and the next migration runs. The error is returned at the end and handled by pending actions
err = migrator.MigrateIngresses(*environment, kubeclient)
if err != nil {
return err
}
for _, migration := range kubernetesMigrations {
if err := migration(); err != nil {
latestErr = err
}
}
return latestErr
return nil
case endpointutils.IsDockerEndpoint(environment):
// get the docker client for the environment, and skip all docker migrations if there's an error
dockerClient, err := migrator.dockerFactory.CreateClient(environment, "", nil)
if err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error creating docker client for environment")
return err
}
defer logs.CloseAndLogErr(dockerClient)
if err := migrator.MigrateGPUs(*environment, dockerClient); err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error migrating GPUs for environment")
log.Error().Err(err).Msgf("Error creating docker client for environment: %d", environment.ID)
return err
}
defer dockerClient.Close()
migrator.MigrateGPUs(*environment, dockerClient)
}
return nil
}
func (migrator *PostInitMigrator) MigrateRegistrySASecrets(environment portainer.Endpoint, kubeclient *cli.KubeClient) error {
if !environment.PostInitMigrations.MigrateRegistrySASecrets {
return nil
}
log.Debug().
Int("endpoint_id", int(environment.ID)).
Msg("migrating registry SA secrets for environment")
return migrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
env, err := tx.Endpoint().Endpoint(environment.ID)
if err != nil {
return err
}
if !env.PostInitMigrations.MigrateRegistrySASecrets {
return nil
}
registries, err := tx.Registry().ReadAll()
if err != nil {
return err
}
for _, registry := range registries {
access, ok := registry.RegistryAccesses[env.ID]
if !ok || len(access.Namespaces) == 0 {
continue
}
secretName := registryutils.RegistrySecretName(registry.ID)
for _, namespace := range access.Namespaces {
if err := kubeclient.AddImagePullSecretToServiceAccount(namespace, "default", secretName); err != nil {
log.Warn().
Err(err).
Int("endpoint_id", int(env.ID)).
Str("namespace", namespace).
Str("secret", secretName).
Msg("failed to add imagePullSecret to service account during registry SA secret migration")
}
}
}
env.PostInitMigrations.MigrateRegistrySASecrets = false
return tx.Endpoint().UpdateEndpoint(env.ID, env)
})
}
func (migrator *PostInitMigrator) MigrateIngresses(environment portainer.Endpoint, kubeclient *cli.KubeClient) error {
// Early exit if we do not need to migrate!
if !environment.PostInitMigrations.MigrateIngresses {
return nil
}
log.Debug().Msgf("Migrating ingresses for environment %d", environment.ID)
log.Debug().
Int("endpoint_id", int(environment.ID)).
Msg("migrating ingresses for environment")
if err := migrator.kubeFactory.MigrateEndpointIngresses(&environment, migrator.dataStore, kubeclient); err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error migrating ingresses for environment")
err := migrator.kubeFactory.MigrateEndpointIngresses(&environment, migrator.dataStore, kubeclient)
if err != nil {
log.Error().Err(err).Msgf("Error migrating ingresses for environment %d", environment.ID)
return err
}
return nil
}
@@ -269,42 +160,29 @@ func (migrator *PostInitMigrator) MigrateGPUs(e portainer.Endpoint, dockerClient
return migrator.dataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
environment, err := tx.Endpoint().Endpoint(e.ID)
if err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(e.ID)).
Msg("error getting environment")
log.Error().Err(err).Msgf("Error getting environment %d", e.ID)
return err
}
// Early exit if we do not need to migrate!
if !environment.PostInitMigrations.MigrateGPUs {
return nil
}
log.Debug().Msgf("Migrating GPUs for environment %d", e.ID)
log.Debug().
Int("endpoint_id", int(e.ID)).
Msg("migrating GPUs for environment")
// Get all containers
// get all containers
containers, err := dockerClient.ContainerList(context.Background(), container.ListOptions{All: true})
if err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("failed to list containers for environment")
log.Error().Err(err).Msgf("failed to list containers for environment %d", environment.ID)
return err
}
// Check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole environment
// check for a gpu on each container. If even one GPU is found, set EnableGPUManagement to true for the whole environment
containersLoop:
for _, container := range containers {
// https://www.sobyte.net/post/2022-10/go-docker/ has nice documentation on the docker client with GPUs
containerDetails, err := dockerClient.ContainerInspect(context.Background(), container.ID)
if err != nil {
log.Error().Err(err).Msg("failed to inspect container")
continue
}
@@ -318,14 +196,10 @@ func (migrator *PostInitMigrator) MigrateGPUs(e portainer.Endpoint, dockerClient
}
}
// Set the MigrateGPUs flag to false so we don't run this again
// set the MigrateGPUs flag to false so we don't run this again
environment.PostInitMigrations.MigrateGPUs = false
if err := tx.Endpoint().UpdateEndpoint(environment.ID, environment); err != nil {
log.Error().
Err(err).
Int("endpoint_id", int(environment.ID)).
Msg("error updating EnableGPUManagement flag for environment")
log.Error().Err(err).Msgf("Error updating EnableGPUManagement flag for environment %d", environment.ID)
return err
}
@@ -18,14 +18,12 @@ import (
)
func TestMigrateGPUs(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/containers/json") {
containerSummary := []container.Summary{{ID: "container1"}}
if err := json.NewEncoder(w).Encode(containerSummary); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
err := json.NewEncoder(w).Encode(containerSummary)
require.NoError(t, err)
return
}
@@ -43,9 +41,8 @@ func TestMigrateGPUs(t *testing.T) {
},
}
if err := json.NewEncoder(w).Encode(container); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
err := json.NewEncoder(w).Encode(container)
require.NoError(t, err)
}))
defer srv.Close()
@@ -80,7 +77,6 @@ func TestMigrateGPUs(t *testing.T) {
}
func TestPostInitMigrate_PendingActionsCreated(t *testing.T) {
t.Parallel()
tests := []struct {
name string
existingPendingActions []*portainer.PendingAction
@@ -133,12 +129,12 @@ func TestPostInitMigrate_PendingActionsCreated(t *testing.T) {
EdgeID: "edgeID",
}
err := store.Endpoint().Create(endpoint)
require.NoError(t, err, "error creating endpoint")
is.NoError(err, "error creating endpoint")
// Create any existing pending actions
for _, action := range tt.existingPendingActions {
err = store.PendingActions().Create(action)
require.NoError(t, err, "error creating pending action")
is.NoError(err, "error creating pending action")
}
migrator := NewPostInitMigrator(
@@ -150,11 +146,11 @@ func TestPostInitMigrate_PendingActionsCreated(t *testing.T) {
)
err = migrator.PostInitMigrate()
require.NoError(t, err, "PostInitMigrate should not return error")
is.NoError(err, "PostInitMigrate should not return error")
// Verify the results
pendingActions, err := store.PendingActions().ReadAll()
require.NoError(t, err, "error reading pending actions")
is.NoError(err, "error reading pending actions")
is.Len(pendingActions, tt.expectedPendingActions, "unexpected number of pending actions")
// If we expect any actions, verify at least one has the expected action type
@@ -164,11 +160,9 @@ func TestPostInitMigrate_PendingActionsCreated(t *testing.T) {
if action.Action == tt.expectedAction {
hasExpectedAction = true
is.Equal(endpoint.ID, action.EndpointID, "action should reference correct endpoint")
break
}
}
is.True(hasExpectedAction, "should have found action of expected type")
}
})
+26 -70
View File
@@ -391,16 +391,16 @@ type storeExport struct {
ResourceControl []portainer.ResourceControl `json:"resource_control,omitempty"`
Role []portainer.Role `json:"roles,omitempty"`
Schedules []portainer.Schedule `json:"schedules,omitempty"`
Settings portainer.Settings `json:"settings,omitzero"`
Settings portainer.Settings `json:"settings,omitempty"`
Snapshot []portainer.Snapshot `json:"snapshots,omitempty"`
SSLSettings portainer.SSLSettings `json:"ssl,omitzero"`
SSLSettings portainer.SSLSettings `json:"ssl,omitempty"`
Stack []portainer.Stack `json:"stacks,omitempty"`
Tag []portainer.Tag `json:"tags,omitempty"`
TeamMembership []portainer.TeamMembership `json:"team_membership,omitempty"`
Team []portainer.Team `json:"teams,omitempty"`
TunnelServer portainer.TunnelServerInfo `json:"tunnel_server,omitzero"`
TunnelServer portainer.TunnelServerInfo `json:"tunnel_server,omitempty"`
User []portainer.User `json:"users,omitempty"`
Version models.Version `json:"version,omitzero"`
Version models.Version `json:"version,omitempty"`
Webhook []portainer.Webhook `json:"webhooks,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
}
@@ -625,129 +625,85 @@ func (store *Store) Import(filename string) (err error) {
return err
}
err = store.Version().UpdateVersion(&backup.Version)
if err != nil {
return err
}
store.Version().UpdateVersion(&backup.Version)
for _, v := range backup.CustomTemplate {
if err := store.CustomTemplate().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the custom template in the database")
}
store.CustomTemplate().Update(v.ID, &v)
}
for _, v := range backup.EdgeGroup {
if err := store.EdgeGroup().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the edge group in the database")
}
store.EdgeGroup().Update(v.ID, &v)
}
for _, v := range backup.EdgeJob {
if err := store.EdgeJob().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the edge job in the database")
}
store.EdgeJob().Update(v.ID, &v)
}
for _, v := range backup.EdgeStack {
if err := store.EdgeStack().UpdateEdgeStack(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the edge stack in the database")
}
store.EdgeStack().UpdateEdgeStack(v.ID, &v)
}
for _, v := range backup.Endpoint {
if err := store.Endpoint().UpdateEndpoint(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the endpoint in the database")
}
store.Endpoint().UpdateEndpoint(v.ID, &v)
}
for _, v := range backup.EndpointGroup {
if err := store.EndpointGroup().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the endpoint group in the database")
}
store.EndpointGroup().Update(v.ID, &v)
}
for _, v := range backup.EndpointRelation {
if err := store.EndpointRelation().UpdateEndpointRelation(v.EndpointID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the endpoint relation in the database")
}
store.EndpointRelation().UpdateEndpointRelation(v.EndpointID, &v)
}
for _, v := range backup.HelmUserRepository {
if err := store.HelmUserRepository().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the helm user repository in the database")
}
store.HelmUserRepository().Update(v.ID, &v)
}
for _, v := range backup.Registry {
if err := store.Registry().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the registry in the database")
}
store.Registry().Update(v.ID, &v)
}
for _, v := range backup.ResourceControl {
if err := store.ResourceControl().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the resource control in the database")
}
store.ResourceControl().Update(v.ID, &v)
}
for _, v := range backup.Role {
if err := store.Role().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the role in the database")
}
store.Role().Update(v.ID, &v)
}
if err := store.Settings().UpdateSettings(&backup.Settings); err != nil {
log.Warn().Err(err).Msg("failed to update the settings in the database")
}
if err := store.SSLSettings().UpdateSettings(&backup.SSLSettings); err != nil {
log.Warn().Err(err).Msg("failed to update the SSL settings in the database")
}
store.Settings().UpdateSettings(&backup.Settings)
store.SSLSettings().UpdateSettings(&backup.SSLSettings)
for _, v := range backup.Snapshot {
if err := store.Snapshot().Update(v.EndpointID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the snapshot in the database")
}
store.Snapshot().Update(v.EndpointID, &v)
}
for _, v := range backup.Stack {
if err := store.Stack().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the stack in the database")
}
store.Stack().Update(v.ID, &v)
}
for _, v := range backup.Tag {
if err := store.Tag().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the tag in the database")
}
store.Tag().Update(v.ID, &v)
}
for _, v := range backup.TeamMembership {
if err := store.TeamMembership().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the team membership in the database")
}
store.TeamMembership().Update(v.ID, &v)
}
for _, v := range backup.Team {
if err := store.Team().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the team in the database")
}
store.Team().Update(v.ID, &v)
}
if err := store.TunnelServer().UpdateInfo(&backup.TunnelServer); err != nil {
log.Warn().Err(err).Msg("failed to update the tunnel server info in the database")
}
store.TunnelServer().UpdateInfo(&backup.TunnelServer)
for _, user := range backup.User {
if err := store.User().Update(user.ID, &user); err != nil {
log.Warn().Str("user", fmt.Sprintf("%+v", user)).Err(err).Msg("failed to update the user in the database")
log.Debug().Str("user", fmt.Sprintf("%+v", user)).Err(err).Msg("failed to update the user in the database")
}
}
for _, v := range backup.Webhook {
if err := store.Webhook().Update(v.ID, &v); err != nil {
log.Warn().Err(err).Msg("failed to update the webhook in the database")
}
store.Webhook().Update(v.ID, &v)
}
return store.connection.RestoreMetadata(backup.Metadata)
+1 -3
View File
@@ -74,9 +74,7 @@ func (tx *StoreTx) Snapshot() dataservices.SnapshotService {
return tx.store.SnapshotService.Tx(tx.tx)
}
func (tx *StoreTx) SSLSettings() dataservices.SSLSettingsService {
return tx.store.SSLSettingsService.Tx(tx.tx)
}
func (tx *StoreTx) SSLSettings() dataservices.SSLSettingsService { return nil }
func (tx *StoreTx) Stack() dataservices.StackService {
return tx.store.StackService.Tx(tx.tx)
@@ -80,17 +80,16 @@
"Name": "local",
"PostInitMigrations": {
"MigrateGPUs": true,
"MigrateIngresses": true,
"MigrateRegistrySASecrets": false
"MigrateIngresses": true
},
"PublicURL": "",
"QueryDate": 0,
"SecuritySettings": {
"allowBindMountsForRegularUsers": true,
"allowContainerCapabilitiesForRegularUsers": true,
"allowDeviceMappingForRegularUsers": true,
"allowHostNamespaceForRegularUsers": true,
"allowPrivilegedModeForRegularUsers": true,
"allowSecurityOptForRegularUsers": false,
"allowStackManagementForRegularUsers": true,
"allowSysctlSettingForRegularUsers": false,
"allowVolumeBrowserForRegularUsers": false,
@@ -605,6 +604,7 @@
"EdgeAgentCheckinInterval": 5,
"EdgePortainerUrl": "",
"EnableEdgeComputeFeatures": false,
"EnableTelemetry": true,
"EnforceEdgeID": false,
"FeatureFlagSettings": null,
"GlobalDeploymentOptions": {
@@ -615,7 +615,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.41.0",
"KubectlShellImage": "portainer/kubectl-shell:2.33.8",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -808,7 +808,6 @@
"AutoUpdate": null,
"CreatedBy": "",
"CreationDate": 0,
"DeploymentStartStatus": 0,
"EndpointId": 1,
"EntryPoint": "docker/alpine37-compose.yml",
"Env": [],
@@ -831,7 +830,6 @@
"AutoUpdate": null,
"CreatedBy": "",
"CreationDate": 0,
"DeploymentStartStatus": 0,
"EndpointId": 1,
"EntryPoint": "docker-compose.yml",
"Env": [],
@@ -854,7 +852,6 @@
"AutoUpdate": null,
"CreatedBy": "",
"CreationDate": 0,
"DeploymentStartStatus": 0,
"EndpointId": 1,
"EntryPoint": "docker-compose.yml",
"Env": [],
@@ -947,7 +944,7 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.41.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.33.8\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
},
"webhooks": null
}
+2 -8
View File
@@ -11,7 +11,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/rs/zerolog/log"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
@@ -144,16 +143,11 @@ func (t *NodeNameTransport) RoundTrip(req *http.Request) (*http.Response, error)
body, err := io.ReadAll(resp.Body)
if err != nil {
if err := resp.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
resp.Body.Close()
return resp, err
}
if err := resp.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
resp.Body.Close()
resp.Body = io.NopCloser(bytes.NewReader(body))

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