Compare commits

..

1 Commits

Author SHA1 Message Date
Oscar Zhou
4f4e8b5af9 fix(snapshot): prevent from returning SnapshotRaw data [BE-12431] (#1442) 2025-11-27 09:01:01 +13:00
1013 changed files with 27777 additions and 51228 deletions

View File

@@ -17,7 +17,7 @@ plugins:
- import
parserOptions:
ecmaVersion: latest
ecmaVersion: 2018
sourceType: module
project: './tsconfig.json'
ecmaFeatures:
@@ -114,13 +114,7 @@ overrides:
'@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
'jsx-a11y/label-has-associated-control': ['error', { 'assert': 'either', controlComponents: ['Input', 'Checkbox'] }]
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
'react/jsx-no-bind': off
'no-await-in-loop': 'off'
@@ -139,18 +133,15 @@ overrides:
'react/jsx-props-no-spreading': off
- files:
- app/**/*.test.*
plugins:
- '@vitest'
extends:
- 'plugin:@vitest/legacy-recommended'
- 'plugin:vitest/recommended'
env:
'@vitest/env': true
'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
'@vitest/no-conditional-expect': warn
- files:
- app/**/*.stories.*
rules:
@@ -158,4 +149,3 @@ overrides:
'@typescript-eslint/no-restricted-imports': off
no-restricted-imports: off
'react/jsx-props-no-spreading': off
'storybook/no-renderer-packages': off

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,15 +94,8 @@ 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.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
.gitignore vendored
View File

@@ -18,5 +18,3 @@ api/docs
.env
go.work.sum
.vitest

View File

@@ -6,7 +6,7 @@ linters:
settings:
forbidigo:
forbid:
- pattern: ^dataservices.DataStore.(EdgeGroup|EdgeJob|EdgeStack|EndpointRelation|Endpoint|GitCredential|Registry|ResourceControl|Role|Settings|Snapshot|SSLSettings|Stack|Tag|User)$
- pattern: ^dataservices.DataStore.(EdgeGroup|EdgeJob|EdgeStack|EndpointRelation|Endpoint|GitCredential|Registry|ResourceControl|Role|Settings|Snapshot|Stack|Tag|User)$
msg: Use a transaction instead
analyze-types: true
exclusions:

View File

@@ -1,14 +1,10 @@
version: "2"
run:
allow-parallel-runners: true
linters:
default: none
enable:
- bodyclose
- copyloopvar
- depguard
- errcheck
- errorlint
- forbidigo
- govet
@@ -21,14 +17,8 @@ linters:
- durationcheck
- errorlint
- govet
- usetesting
- zerologlint
- testifylint
- modernize
- unconvert
- unused
- zerologlint
- exptostd
settings:
staticcheck:
checks: ["all", "-ST1003", "-ST1005", "-ST1016", "-SA1019", "-QF1003"]
@@ -52,10 +42,6 @@ linters:
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.v3
desc: use go.yaml.in/yaml/v3 instead
forbidigo:
forbid:
- pattern: ^tls\.Config$
@@ -73,13 +59,12 @@ linters:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
exclusions:
generated: lax
paths:

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
cd $(dirname -- "$0") && pnpm lint-staged
cd $(dirname -- "$0") && yarn lint-staged

View File

@@ -1,3 +1,2 @@
dist
api/datastore/test_data
coverage
api/datastore/test_data

View File

@@ -9,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,
},
},
},
],

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,30 +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: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers,
},
},
};
];
export default preview;
export const loaders = [mswLoader];

View File

@@ -1,44 +0,0 @@
# Portainer Community Edition
Open-source container management platform with full Docker and Kubernetes support.
see also:
- docs/guidelines/server-architecture.md
- docs/guidelines/go-conventions.md
- docs/guidelines/typescript-conventions.md
## Package Manager
- **PNPM** 10+ (for frontend)
- **Go** 1.25.7 (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
pnpm run dev # Webpack dev server
pnpm run build # Build frontend with webpack
pnpm run test # Run frontend tests
# 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)

View File

@@ -77,7 +77,7 @@ The feature request process is similar to the bug report process but has an extr
## Build and run Portainer locally
Ensure you have Docker, Node.js, pnpm, and Golang installed in the correct versions.
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
Install dependencies:

View File

@@ -20,7 +20,7 @@ all: tidy deps build-server build-client ## Build the client, server and downloa
build-all: all ## Alias for the 'all' target (used by CI)
build-client: init-dist ## Build the client
export NODE_ENV=$(ENV) && pnpm run build --config $(WEBPACK_CONFIG)
export NODE_ENV=$(ENV) && yarn build --config $(WEBPACK_CONFIG)
build-server: init-dist ## Build the server binary
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
@@ -29,7 +29,7 @@ build-image: build-all ## Build the Portainer image locally
docker buildx build --load -t portainerci/portainer-ce:$(TAG) -f build/linux/Dockerfile .
build-storybook: ## Build and serve the storybook files
pnpm run storybook:build
yarn storybook:build
##@ Build dependencies
.PHONY: deps server-deps client-deps tidy
@@ -39,7 +39,7 @@ server-deps: init-dist ## Download dependant server binaries
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
client-deps: ## Install client dependencies
pnpm install
yarn
tidy: ## Tidy up the go.mod file
@go mod tidy
@@ -55,7 +55,7 @@ clean: ## Remove all build and download artifacts
test: test-server test-client ## Run all tests
test-client: ## Run client tests
pnpm run test $(ARGS) --coverage
yarn test $(ARGS) --coverage
test-server: ## Run server tests
$(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover -covermode=atomic -coverprofile=coverage.out ./...
@@ -67,7 +67,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
yarn dev
dev-server: build-server ## Run the server in development mode
@./dev/run_container.sh
@@ -81,7 +81,7 @@ dev-server-podman: build-server ## Run the server in development mode
format: format-client format-server ## Format all code
format-client: ## Format client code
pnpm run format
yarn format
format-server: ## Format server code
go fmt ./...
@@ -91,7 +91,7 @@ format-server: ## Format server code
lint: lint-client lint-server ## Lint all code
lint-client: ## Lint client code
pnpm run lint
yarn lint
lint-server: tidy ## Lint server code
golangci-lint run --timeout=10m -c .golangci.yaml
@@ -105,12 +105,11 @@ dev-extension: build-server build-client ## Run the extension in development mod
##@ Docs
.PHONY: docs-build docs-validate docs-clean docs-validate-clean
docs-build: init-dist ## Build docs
go mod download -x
cd api && $(SWAG) init -o "../dist/docs" -ot "yaml" -g ./http/handler/handler.go --parseDependency --parseInternal --parseDepth 2 -p pascalcase --markdownFiles ./
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
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
yarn swagger-cli validate dist/docs/openapi.yaml
##@ Helpers
.PHONY: help

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

View File

@@ -1,57 +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**: Email 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)

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)

View File

@@ -157,10 +157,7 @@ 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)

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.

View File

@@ -9,9 +9,6 @@ import (
"os"
"path/filepath"
"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 +20,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 +86,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)
@@ -109,7 +105,7 @@ func ExtractTarGz(r io.Reader, outputDirPath string) error {
case tar.TypeDir:
// skip, dir will be created with a file
case tar.TypeReg:
p := filesystem.JoinPaths(outputDirPath, header.Name)
p := filepath.Clean(filepath.Join(outputDirPath, header.Name))
if err := os.MkdirAll(filepath.Dir(p), 0o744); err != nil {
return fmt.Errorf("Failed to extract dir %s", filepath.Dir(p))
}
@@ -120,7 +116,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,

View File

@@ -1,16 +1,12 @@
package archive
import (
"archive/tar"
"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"
)
@@ -18,7 +14,7 @@ import (
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
}
@@ -26,9 +22,7 @@ func listFiles(dir string) []string {
items = append(items, path)
return nil
}); err != nil {
log.Warn().Err(err).Msg("failed to list files in directory")
}
})
return items
}
@@ -40,7 +34,7 @@ func Test_shouldCreateArchive(t *testing.T) {
err := os.WriteFile(path.Join(tmpdir, "outer"), content, 0600)
require.NoError(t, err)
err = os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
os.MkdirAll(path.Join(tmpdir, "dir"), 0700)
require.NoError(t, err)
err = os.WriteFile(path.Join(tmpdir, "dir", ".dotfile"), content, 0600)
@@ -111,56 +105,3 @@ func Test_shouldCreateArchive2(t *testing.T) {
wasExtracted("dir/inner")
wasExtracted("dir/.dotfile")
}
func TestExtractTarGzPathTraversal(t *testing.T) {
testDir := t.TempDir()
// Create an evil file with a path traversal attempt
tarPath := filesystem.JoinPaths(testDir, "evil.tar.gz")
evilFile, err := os.Create(tarPath)
require.NoError(t, err)
gzWriter := gzip.NewWriter(evilFile)
tarWriter := tar.NewWriter(gzWriter)
content := []byte("evil content")
header := &tar.Header{
Name: "../evil.txt",
Mode: 0600,
Size: int64(len(content)),
Typeflag: tar.TypeReg,
}
err = tarWriter.WriteHeader(header)
require.NoError(t, err)
_, err = tarWriter.Write(content)
require.NoError(t, err)
err = tarWriter.Close()
require.NoError(t, err)
err = gzWriter.Close()
require.NoError(t, err)
err = evilFile.Close()
require.NoError(t, err)
// Attempt to extract the evil file
extractionDir := filesystem.JoinPaths(testDir, "extraction")
err = os.Mkdir(extractionDir, 0700)
require.NoError(t, err)
tarFile, err := os.Open(tarPath)
require.NoError(t, err)
// Check that the file didn't escape
err = ExtractTarGz(tarFile, extractionDir)
require.NoError(t, err)
require.NoFileExists(t, filesystem.JoinPaths(testDir, "evil.txt"))
err = tarFile.Close()
require.NoError(t, err)
}

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")

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

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
}

View File

@@ -1,253 +0,0 @@
package ecr
import (
"testing"
)
func TestParseECREndpoint(t *testing.T) {
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)
}
})
}
}

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
}

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
}

View File

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

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
}

View File

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

View File

@@ -1,4 +1,5 @@
//go:build !windows
// +build !windows
package cli

View File

@@ -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
@@ -348,7 +347,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
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 url for trusted origin. Please check the trusted origins flag.")
}
@@ -530,9 +529,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
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 {
@@ -633,7 +630,7 @@ func main() {
Str("build_number", build.BuildNumber).
Str("image_tag", build.ImageTag).
Str("nodejs_version", build.NodejsVersion).
Str("pnpm_version", build.PnpmVersion).
Str("yarn_version", build.YarnVersion).
Str("webpack_version", build.WebpackVersion).
Str("go_version", build.GoVersion).
Msg("starting Portainer")

View File

@@ -164,9 +164,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 +235,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

View File

@@ -9,7 +9,6 @@ import (
"path/filepath"
"testing"
"github.com/portainer/portainer/api/logs"
"github.com/portainer/portainer/pkg/fips"
"github.com/stretchr/testify/assert"
@@ -48,17 +47,16 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
)
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))
err := encrypt(originFile, encryptedFileWriter, []byte(passphrase))
require.NoError(t, err, "Failed to encrypt a file")
logs.CloseAndLogErr(encryptedFileWriter)
encryptedFileWriter.Close()
encryptedContent, err := os.ReadFile(encryptedFilePath)
require.NoError(t, err, "Couldn't read encrypted file")
@@ -66,11 +64,11 @@ func Test_encryptAndDecrypt_withTheSamePassword(t *testing.T) {
encryptedFileReader, err := os.Open(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileReader)
defer encryptedFileReader.Close()
decryptedFileWriter, err := os.Create(decryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(decryptedFileWriter)
defer decryptedFileWriter.Close()
decryptedReader, err := decrypt(encryptedFileReader, []byte(passphrase))
if !decryptShouldSucceed {
@@ -78,11 +76,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")
}
}
@@ -153,40 +149,33 @@ func Test_encryptAndDecrypt_withStrongPassphrase(t *testing.T) {
)
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))
err := encrypt(originFile, encryptedFileWriter, []byte(passphrase))
require.NoError(t, err, "Failed to encrypt a file")
logs.CloseAndLogErr(encryptedFileWriter)
encryptedFileWriter.Close()
encryptedContent, err := os.ReadFile(encryptedFilePath)
require.NoError(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")
_, 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")
}
@@ -210,19 +199,16 @@ func Test_encryptAndDecrypt_withTheSamePasswordSmallFile(t *testing.T) {
)
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"))
err := encrypt(originFile, encryptedFileWriter, []byte("passphrase"))
require.NoError(t, err, "Failed to encrypt a file")
logs.CloseAndLogErr(encryptedFileWriter)
encryptedFileWriter.Close()
encryptedContent, err := os.ReadFile(encryptedFilePath)
require.NoError(t, err, "Couldn't read encrypted file")
@@ -230,11 +216,11 @@ func Test_encryptAndDecrypt_withTheSamePasswordSmallFile(t *testing.T) {
encryptedFileReader, err := os.Open(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileReader)
defer encryptedFileReader.Close()
decryptedFileWriter, err := os.Create(decryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(decryptedFileWriter)
defer decryptedFileWriter.Close()
decryptedReader, err := decrypt(encryptedFileReader, []byte("passphrase"))
require.NoError(t, err, "Failed to decrypt file")
@@ -272,11 +258,11 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
originFile, err := os.Open(originFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(originFile)
defer originFile.Close()
encryptedFileWriter, err := os.Create(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileWriter)
defer encryptedFileWriter.Close()
err = encrypt(originFile, encryptedFileWriter, []byte(""))
require.NoError(t, err, "Failed to encrypt a file")
@@ -287,11 +273,11 @@ func Test_encryptAndDecrypt_withEmptyPassword(t *testing.T) {
encryptedFileReader, err := os.Open(encryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(encryptedFileReader)
defer encryptedFileReader.Close()
decryptedFileWriter, err := os.Create(decryptedFilePath)
require.NoError(t, err)
defer logs.CloseAndLogErr(decryptedFileWriter)
defer decryptedFileWriter.Close()
decryptedReader, err := decrypt(encryptedFileReader, []byte(""))
require.NoError(t, err, "Failed to decrypt file")
@@ -324,30 +310,25 @@ func Test_decryptWithDifferentPassphrase_shouldProduceWrongResult(t *testing.T)
)
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"))
err := encrypt(originFile, encryptedFileWriter, []byte("passphrase"))
require.NoError(t, err, "Failed to encrypt a file")
encryptedContent, err := os.ReadFile(encryptedFilePath)
require.NoError(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")

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
}

View File

@@ -98,36 +98,18 @@ func Test_NeedsEncryptionMigration(t *testing.T) {
// Special case. If portainer.db and portainer.edb exist.
dbFile1 := path.Join(connection.Path, DatabaseFileName)
f, _ := os.Create(dbFile1)
err := f.Close()
require.NoError(t, err)
defer func() {
err := os.Remove(dbFile1)
require.NoError(t, err)
}()
f.Close()
defer os.Remove(dbFile1)
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 := 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 {
@@ -154,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
})

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 {

View File

@@ -45,12 +45,12 @@ func (connection *DbConnection) UnmarshalObject(data []byte, object any) error {
}
}
if err := json.Unmarshal(data, object); err != nil {
if e := json.Unmarshal(data, object); e != nil {
// Special case for the VERSION bucket. Here we're not using json
// So we need to return it as a string
s, ok := object.(*string)
if !ok {
return errors.Wrap(err, "Failed unmarshalling object")
return errors.Wrap(err, e.Error())
}
*s = string(data)

View File

@@ -17,7 +17,7 @@ import (
)
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"
)

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,55 +17,70 @@ type testStruct struct {
}
func TestTxs(t *testing.T) {
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)
@@ -76,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 {
@@ -94,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")
}
}

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,6 +50,7 @@ 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) {
service := BaseDataService[testObject, int]{
Bucket: "testBucket",

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
}

View File

@@ -5,7 +5,6 @@ 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"
)
@@ -15,7 +14,7 @@ func TestUpdate(t *testing.T) {
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)

View File

@@ -7,7 +7,6 @@ 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"
)
@@ -21,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)
@@ -110,7 +109,7 @@ func TestAddEndpointRelationsForEdgeStack(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)
@@ -129,7 +128,7 @@ func TestEndpointRelations(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)

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")
)

View File

@@ -223,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)
}

View File

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

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

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

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)
}

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)

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)

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
}

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 {

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
}

View File

@@ -1,11 +1,9 @@
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"
@@ -38,12 +36,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)
@@ -59,14 +53,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)
@@ -76,65 +66,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) {
_, 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) {
_, 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))
})
}

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")

View File

@@ -51,13 +51,13 @@ func TestStoreFull(t *testing.T) {
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 {
@@ -90,7 +90,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 {
@@ -131,9 +142,7 @@ 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")
@@ -142,14 +151,13 @@ func (store *Store) CreateEndpoint(t *testing.T, name string, endpointType porta
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) {
@@ -161,8 +169,7 @@ 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")
@@ -275,8 +282,7 @@ 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")

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{

View File

@@ -14,7 +14,6 @@ import (
"github.com/portainer/portainer/api/database/boltdb"
"github.com/portainer/portainer/api/database/models"
"github.com/portainer/portainer/api/datastore/migrator"
"github.com/stretchr/testify/require"
"github.com/google/go-cmp/cmp"
"github.com/rs/zerolog/log"
@@ -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
@@ -177,14 +158,14 @@ func TestRollback(t *testing.T) {
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("")
}
@@ -203,9 +184,7 @@ func TestRollback(t *testing.T) {
return
}
_, err = store.Open()
require.NoError(t, err)
store.Open()
testVersion(store, version, t)
})
@@ -218,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("")
}
@@ -241,8 +218,7 @@ func TestRollback(t *testing.T) {
return
}
_, err = store.Open()
require.NoError(t, err)
store.Open()
testVersion(store, version, t)
})
}
@@ -261,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
}
@@ -284,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
}
}
@@ -293,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,15 +302,11 @@ 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 := filepath.Join(os.TempDir(), "portainer-migrator-test-fail.json")
err = os.WriteFile(
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,

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
}

View File

@@ -6,7 +6,6 @@ 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"
)
@@ -16,7 +15,7 @@ func TestMigrateEdgeGroupEndpointsToRoars_2_33_0Idempotency(t *testing.T) {
err := conn.Open()
require.NoError(t, err)
defer logs.CloseAndLogErr(conn)
defer conn.Close()
edgeGroupService, err := edgegroup.NewService(conn)
require.NoError(t, err)

View File

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

View File

@@ -21,6 +21,7 @@ func (m *Migrator) updateSettingsToDB25() error {
}
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
legacySettings.EnableTelemetry = true
legacySettings.AllowContainerCapabilitiesForRegularUsers = true

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,
}
}
}

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 {
@@ -17,10 +16,7 @@ func Test_ConvertCleanNAPWithOverridePoliciesPayload(t *testing.T) {
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)
@@ -96,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)
}
})
}

View File

@@ -11,7 +11,6 @@ import (
dockerClient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/internal/endpointutils"
"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"
@@ -90,7 +89,6 @@ func (postInitMigrator *PostInitMigrator) createPostInitMigrationPendingAction(e
EndpointID: environmentID,
Action: actions.PostInitMigrateEnvironment,
}
pendingActions, err := postInitMigrator.dataStore.PendingActions().ReadAll()
if err != nil {
return fmt.Errorf("failed to retrieve pending actions: %w", err)
@@ -121,12 +119,11 @@ func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endp
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
if err := migrator.MigrateIngresses(*environment, kubeclient); err != nil {
err = migrator.MigrateIngresses(*environment, kubeclient)
if err != nil {
return err
}
return nil
case endpointutils.IsDockerEndpoint(environment):
// get the docker client for the environment, and skip all docker migrations if there's an error
@@ -135,11 +132,8 @@ func (migrator *PostInitMigrator) MigrateEnvironment(environment *portainer.Endp
log.Error().Err(err).Msgf("Error creating docker client for environment: %d", environment.ID)
return err
}
defer logs.CloseAndLogErr(dockerClient)
if err := migrator.MigrateGPUs(*environment, dockerClient); err != nil {
return err
}
defer dockerClient.Close()
migrator.MigrateGPUs(*environment, dockerClient)
}
return nil

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)

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)

View File

@@ -89,7 +89,6 @@
"allowDeviceMappingForRegularUsers": true,
"allowHostNamespaceForRegularUsers": true,
"allowPrivilegedModeForRegularUsers": true,
"allowSecurityOptForRegularUsers": false,
"allowStackManagementForRegularUsers": true,
"allowSysctlSettingForRegularUsers": false,
"allowVolumeBrowserForRegularUsers": false,
@@ -604,6 +603,7 @@
"EdgeAgentCheckinInterval": 5,
"EdgePortainerUrl": "",
"EnableEdgeComputeFeatures": false,
"EnableTelemetry": true,
"EnforceEdgeID": false,
"FeatureFlagSettings": null,
"GlobalDeploymentOptions": {
@@ -614,7 +614,7 @@
"RequiredPasswordLength": 12
},
"KubeconfigExpiry": "0",
"KubectlShellImage": "portainer/kubectl-shell:2.39.0",
"KubectlShellImage": "portainer/kubectl-shell:2.36.0",
"LDAPSettings": {
"AnonymousMode": true,
"AutoCreateUsers": true,
@@ -943,7 +943,7 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.39.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.36.0\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
},
"webhooks": null
}

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

View File

@@ -8,7 +8,6 @@ import (
"github.com/portainer/portainer/api/dataservices"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/docker/images"
"github.com/portainer/portainer/api/logs"
"github.com/Masterminds/semver"
"github.com/docker/docker/api/types"
@@ -76,7 +75,7 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
if err != nil {
return nil, errors.Wrap(err, "create client error")
}
defer logs.CloseAndLogErr(cli)
defer cli.Close()
log.Debug().Str("container_id", containerId).Msg("starting to fetch container information")
@@ -147,19 +146,13 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
c.sr.push(func() {
log.Debug().Str("container_id", containerId).Str("container", container.Name).Msg("restoring the container")
if err := cli.ContainerRename(ctx, containerId, container.Name); err != nil {
log.Warn().Err(err).Msg("failure to rename container")
}
cli.ContainerRename(ctx, containerId, container.Name)
for _, network := range container.NetworkSettings.Networks {
if err := cli.NetworkConnect(ctx, network.NetworkID, containerId, network); err != nil {
log.Warn().Err(err).Msg("failure to connect container to network")
}
cli.NetworkConnect(ctx, network.NetworkID, containerId, network)
}
if err := cli.ContainerStart(ctx, containerId, dockercontainer.StartOptions{}); err != nil {
log.Warn().Err(err).Msg("failure to start container")
}
cli.ContainerStart(ctx, containerId, dockercontainer.StartOptions{})
})
log.Debug().Str("container", strings.Split(container.Name, "/")[1]).Msg("starting to create a new container")
@@ -182,14 +175,8 @@ func (c *ContainerService) Recreate(ctx context.Context, endpoint *portainer.End
c.sr.push(func() {
log.Debug().Str("container_id", create.ID).Msg("removing the new container")
if err := cli.ContainerStop(ctx, create.ID, dockercontainer.StopOptions{}); err != nil {
log.Warn().Err(err).Msg("failure to stop container")
}
if err := cli.ContainerRemove(ctx, create.ID, dockercontainer.RemoveOptions{}); err != nil {
log.Warn().Err(err).Msg("failure to remove container")
}
cli.ContainerStop(ctx, create.ID, dockercontainer.StopOptions{})
cli.ContainerRemove(ctx, create.ID, dockercontainer.RemoveOptions{})
})
if err != nil {

View File

@@ -5,7 +5,6 @@ import (
"io"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/logs"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"
@@ -43,7 +42,7 @@ func (puller *Puller) Pull(ctx context.Context, img Image) error {
if err != nil {
return err
}
defer logs.CloseAndLogErr(out)
defer out.Close()
_, err = io.ReadAll(out)

View File

@@ -280,7 +280,13 @@ func contains(statuses []Status, status Status) bool {
return false
}
return slices.Contains(statuses, status)
for _, s := range statuses {
if s == status {
return true
}
}
return false
}
func allMatch(statuses []Status, status Status) bool {

View File

@@ -3,7 +3,6 @@ package docker
import (
portainer "github.com/portainer/portainer/api"
dockerclient "github.com/portainer/portainer/api/docker/client"
"github.com/portainer/portainer/api/logs"
"github.com/portainer/portainer/pkg/snapshot"
)
@@ -25,7 +24,7 @@ func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*p
if err != nil {
return nil, err
}
defer logs.CloseAndLogErr(cli)
defer cli.Close()
return snapshot.CreateDockerSnapshot(cli)
}

View File

@@ -6,7 +6,6 @@ import (
"strings"
"sync"
"github.com/containerd/containerd/errdefs"
"github.com/docker/docker/api/types/container"
)
@@ -36,10 +35,8 @@ func CalculateContainerStats(ctx context.Context, cli DockerClient, isSwarm bool
var aggErr error
var aggMu sync.Mutex
var processedCount int
for i := range containers {
id := containers[i].ID
semaphore <- struct{}{}
wg.Go(func() {
defer func() { <-semaphore }()
@@ -47,17 +44,8 @@ func CalculateContainerStats(ctx context.Context, cli DockerClient, isSwarm bool
containerInspection, err := cli.ContainerInspect(ctx, id)
stat := ContainerStats{}
if err != nil {
if errdefs.IsNotFound(err) {
// An edge case is reported that Docker can list containers with no names,
// but when inspecting a container with specific ID and it is not found.
// In this case, we can safely ignore the error.
// ref@https://linear.app/portainer/issue/BE-12567/500-error-when-loading-docker-dashboard-in-portainer
return
}
aggMu.Lock()
aggErr = errors.Join(aggErr, err)
processedCount++
aggMu.Unlock()
return
}
@@ -68,7 +56,6 @@ func CalculateContainerStats(ctx context.Context, cli DockerClient, isSwarm bool
stopped += stat.Stopped
healthy += stat.Healthy
unhealthy += stat.Unhealthy
processedCount++
mu.Unlock()
})
}
@@ -80,7 +67,7 @@ func CalculateContainerStats(ctx context.Context, cli DockerClient, isSwarm bool
Stopped: stopped,
Healthy: healthy,
Unhealthy: unhealthy,
Total: processedCount,
Total: len(containers),
}, aggErr
}

View File

@@ -3,11 +3,9 @@ package stats
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/containerd/containerd/errdefs"
"github.com/docker/docker/api/types/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -39,7 +37,6 @@ func TestCalculateContainerStats(t *testing.T) {
{ID: "container8"},
{ID: "container9"},
{ID: "container10"},
{ID: "container11"},
}
// Setup mock expectations with different container states to test various scenarios
@@ -61,6 +58,7 @@ func TestCalculateContainerStats(t *testing.T) {
{"container10", container.StateDead, nil, ContainerStats{Running: 0, Stopped: 1, Healthy: 0, Unhealthy: 0}},
}
expected := ContainerStats{}
// Setup mock expectations for all containers with artificial delays to simulate real Docker calls
for _, state := range containerStates {
mockClient.On("ContainerInspect", mock.Anything, state.id).Return(container.InspectResponse{
@@ -70,11 +68,14 @@ func TestCalculateContainerStats(t *testing.T) {
Health: state.health,
},
},
}, nil).After(30 * time.Millisecond) // Simulate 30ms Docker API call
}
}, nil).After(50 * time.Millisecond) // Simulate 50ms Docker API call
// Setup mock expectation for a container that returns NotFound error
mockClient.On("ContainerInspect", mock.Anything, "container11").Return(container.InspectResponse{}, fmt.Errorf("No such container: %w", errdefs.ErrNotFound)).After(50 * time.Millisecond)
expected.Running += state.expected.Running
expected.Stopped += state.expected.Stopped
expected.Healthy += state.expected.Healthy
expected.Unhealthy += state.expected.Unhealthy
expected.Total++
}
// Call the function and measure time
startTime := time.Now()
@@ -83,10 +84,11 @@ func TestCalculateContainerStats(t *testing.T) {
duration := time.Since(startTime)
// Assert results
assert.Equal(t, 6, stats.Running)
assert.Equal(t, 4, stats.Stopped)
assert.Equal(t, 2, stats.Healthy)
assert.Equal(t, 2, stats.Unhealthy)
assert.Equal(t, expected, stats)
assert.Equal(t, expected.Running, stats.Running)
assert.Equal(t, expected.Stopped, stats.Stopped)
assert.Equal(t, expected.Healthy, stats.Healthy)
assert.Equal(t, expected.Unhealthy, stats.Unhealthy)
assert.Equal(t, 10, stats.Total)
// Verify concurrent processing by checking that all mock calls were made

View File

@@ -77,9 +77,6 @@ type (
// CreatedByUserId is the user ID that created this stack
// Used for adding labels to Kubernetes manifests
CreatedByUserId string
// HelmConfig represents the Helm configuration for an edge stack
HelmConfig portainer.HelmConfig
}
DeployerOptionsPayload struct {

View File

@@ -13,7 +13,6 @@ import (
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory"
"github.com/portainer/portainer/api/internal/registryutils"
"github.com/portainer/portainer/api/logs"
"github.com/portainer/portainer/api/stacks/stackutils"
"github.com/portainer/portainer/pkg/libstack"
@@ -181,7 +180,7 @@ func createEnvFile(stack *portainer.Stack) (string, error) {
if err != nil {
return "", err
}
defer logs.CloseAndLogErr(envfile)
defer envfile.Close()
// Copy from default .env file
defaultEnvPath := path.Join(stack.ProjectPath, path.Dir(stack.EntryPoint), ".env")
@@ -206,14 +205,13 @@ func copyDefaultEnvFile(w io.Writer, defaultEnvFilePath string) error {
return nil
}
defer logs.CloseAndLogErr(defaultEnvFile)
defer defaultEnvFile.Close()
if _, err = io.Copy(w, defaultEnvFile); err == nil {
if _, err = fmt.Fprintf(w, "\n"); err != nil {
return fmt.Errorf("failed to copy default env file: %w", err)
}
}
return nil
// If couldn't copy the .env file, then ignore the error and try to continue
}
@@ -225,7 +223,6 @@ func copyConfigEnvVars(w io.Writer, envs []portainer.Pair) error {
return fmt.Errorf("failed to copy config env vars: %w", err)
}
}
return nil
}

View File

@@ -11,7 +11,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/pkg/libstack/compose"
"github.com/portainer/portainer/pkg/testhelpers"
"github.com/stretchr/testify/require"
"github.com/rs/zerolog/log"
)
@@ -26,11 +25,8 @@ const composedContainerName = "compose_wrapper_test"
func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
dir := t.TempDir()
composeFileName := "compose_wrapper_test.yml"
f, err := os.Create(filepath.Join(dir, composeFileName))
require.NoError(t, err)
_, err = f.WriteString(composeFile)
require.NoError(t, err)
f, _ := os.Create(filepath.Join(dir, composeFileName))
f.WriteString(composeFile)
stack := &portainer.Stack{
ProjectPath: dir,
@@ -38,7 +34,11 @@ func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
Name: "project-name",
}
return stack, &portainer.Endpoint{URL: "unix://"}
endpoint := &portainer.Endpoint{
URL: "unix://",
}
return stack, endpoint
}
func Test_UpAndDown(t *testing.T) {

View File

@@ -71,9 +71,7 @@ func Test_createEnvFile(t *testing.T) {
func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
dir := t.TempDir()
err := os.WriteFile(path.Join(dir, ".env"), []byte("VAR1=VAL1\nVAR2=VAL2\n"), 0600)
require.NoError(t, err)
os.WriteFile(path.Join(dir, ".env"), []byte("VAR1=VAL1\nVAR2=VAL2\n"), 0600)
stack := &portainer.Stack{
ProjectPath: dir,
Env: []portainer.Pair{
@@ -85,12 +83,8 @@ func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
assert.Equal(t, filepath.Join(stack.ProjectPath, "stack.env"), result)
require.NoError(t, err)
assert.FileExists(t, path.Join(dir, "stack.env"))
f, err := os.Open(path.Join(dir, "stack.env"))
require.NoError(t, err)
content, err := io.ReadAll(f)
require.NoError(t, err)
f, _ := os.Open(path.Join(dir, "stack.env"))
content, _ := io.ReadAll(f)
assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content)
}

View File

@@ -111,8 +111,8 @@ func (deployer *KubernetesDeployer) command(operation string, userID portainer.U
}
operations := map[string]func(context.Context, []string) (string, error){
"apply": client.ApplyDynamic,
"delete": client.DeleteDynamic,
"apply": client.Apply,
"delete": client.Delete,
}
operationFunc, ok := operations[operation]

View File

@@ -183,7 +183,7 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, config
if !endpoint.TLSConfig.TLSSkipVerify {
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
} else {
args = append(args, "--tlscacert", "")
args = append(args, "--tlscacert", "''")
}
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {

View File

@@ -3,9 +3,7 @@ package exec
import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConfigFilePaths(t *testing.T) {
@@ -15,29 +13,3 @@ func TestConfigFilePaths(t *testing.T) {
output := configureFilePaths(args, filePaths)
assert.ElementsMatch(t, expected, output, "wrong output file paths")
}
func TestPrepareDockerCommandAndArgs(t *testing.T) {
binaryPath := "/test/dist"
configPath := "/test/config"
manager := &SwarmStackManager{
binaryPath: binaryPath,
configPath: configPath,
}
endpoint := &portainer.Endpoint{
URL: "tcp://test:9000",
TLSConfig: portainer.TLSConfiguration{
TLS: true,
TLSSkipVerify: true,
},
}
command, args, err := manager.prepareDockerCommandAndArgs(binaryPath, configPath, endpoint)
require.NoError(t, err)
expectedCommand := "/test/dist/docker"
expectedArgs := []string{"--config", "/test/config", "-H", "tcp://test:9000", "--tls", "--tlscacert", ""}
require.Equal(t, expectedCommand, command)
require.Equal(t, expectedArgs, args)
}

View File

@@ -6,8 +6,6 @@ import (
"os"
"path/filepath"
"strings"
"github.com/portainer/portainer/api/logs"
)
// CopyPath copies file or directory defined by the path to the toDir path
@@ -16,8 +14,6 @@ func CopyPath(path string, toDir string) error {
if err != nil && errors.Is(err, os.ErrNotExist) {
// skip copy if file does not exist
return nil
} else if err != nil {
return err
}
if !info.IsDir() {
@@ -69,7 +65,7 @@ func copyFile(src, dst string) error {
if err != nil {
return err
}
defer logs.CloseAndLogErr(from)
defer from.Close()
// has to include 'execute' bit, otherwise fails. MkdirAll follows `mkdir -m` restrictions
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
@@ -79,7 +75,7 @@ func copyFile(src, dst string) error {
if err != nil {
return err
}
defer logs.CloseAndLogErr(to)
defer to.Close()
_, err = io.Copy(to, from)
return err

View File

@@ -19,15 +19,12 @@ func Test_copyFile_returnsError_whenSourceDoesNotExist(t *testing.T) {
func Test_copyFile_shouldMakeAbackup(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "origin"), content, 0600)
err := os.WriteFile(path.Join(tmpdir, "origin"), content, 0600)
err := copyFile(path.Join(tmpdir, "origin"), path.Join(tmpdir, "copy"))
require.NoError(t, err)
err = copyFile(path.Join(tmpdir, "origin"), path.Join(tmpdir, "copy"))
require.NoError(t, err)
copyContent, err := os.ReadFile(path.Join(tmpdir, "copy"))
require.NoError(t, err)
copyContent, _ := os.ReadFile(path.Join(tmpdir, "copy"))
assert.Equal(t, content, copyContent)
}
@@ -62,14 +59,10 @@ func Test_CopyPath_shouldSkipWhenNotExist(t *testing.T) {
func Test_CopyPath_shouldCopyFile(t *testing.T) {
tmpdir := t.TempDir()
content := []byte("content")
os.WriteFile(path.Join(tmpdir, "file"), content, 0600)
err := os.WriteFile(path.Join(tmpdir, "file"), content, 0600)
require.NoError(t, err)
err = os.MkdirAll(path.Join(tmpdir, "backup"), 0700)
require.NoError(t, err)
err = CopyPath(path.Join(tmpdir, "file"), path.Join(tmpdir, "backup"))
os.MkdirAll(path.Join(tmpdir, "backup"), 0700)
err := CopyPath(path.Join(tmpdir, "file"), path.Join(tmpdir, "backup"))
require.NoError(t, err)
copyContent, err := os.ReadFile(path.Join(tmpdir, "backup", "file"))
@@ -86,20 +79,3 @@ func Test_CopyPath_shouldCopyDir(t *testing.T) {
assert.FileExists(t, filepath.Join(destination, "copy_test", "dir", ".dotfile"))
assert.FileExists(t, filepath.Join(destination, "copy_test", "dir", "inner"))
}
func TestCopyPathPanic(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "myfile")
err := os.WriteFile(p, []byte("contents"), 0644)
require.NoError(t, err)
err = os.Chmod(dir, 0)
require.NoError(t, err)
err = CopyPath(p, t.TempDir())
require.Error(t, err)
err = os.Chmod(dir, 0755)
require.NoError(t, err)
}

View File

@@ -12,7 +12,6 @@ import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/logs"
"github.com/gofrs/uuid"
"github.com/rs/zerolog/log"
@@ -195,7 +194,7 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
return err
}
defer logs.CloseAndLogErr(finput)
defer finput.Close()
exists, err = service.FileExists(toFilePath)
if err != nil {
@@ -218,7 +217,7 @@ func (service *Service) Copy(fromFilePath string, toFilePath string, deleteIfExi
return err
}
defer logs.CloseAndLogErr(foutput)
defer foutput.Close()
buf := make([]byte, 1024)
for {
@@ -703,7 +702,7 @@ func (service *Service) createPEMFileInStore(content []byte, fileType, filePath
if err != nil {
return err
}
defer logs.CloseAndLogErr(out)
defer out.Close()
return pem.Encode(out, block)
}
@@ -1009,7 +1008,7 @@ func CreateFile(path string, r io.Reader) error {
return err
}
defer logs.CloseAndLogErr(out)
defer out.Close()
_, err = io.Copy(out, r)
return err

View File

@@ -30,12 +30,11 @@ func Test_FileExists_whenFileNotExistsShouldReturnFalse(t *testing.T) {
}
func testHelperFileExists_fileExists(t *testing.T, checker func(path string) (bool, error)) {
file, err := os.CreateTemp(t.TempDir(), t.Name())
file, err := os.CreateTemp("", t.Name())
require.NoError(t, err, "CreateTemp should not fail")
t.Cleanup(func() {
err := os.RemoveAll(file.Name())
require.NoError(t, err)
os.RemoveAll(file.Name())
})
exists, err := checker(file.Name())

View File

@@ -58,14 +58,12 @@ func Test_movePath_succesIfOverwriteSetWhenDestinationDirExists(t *testing.T) {
func Test_movePath_successWhenSourceExistsAndDestinationIsMissing(t *testing.T) {
tmp := t.TempDir()
sourceDir := path.Join(tmp, "source")
err := os.Mkdir(sourceDir, 0766)
require.NoError(t, err)
os.Mkdir(sourceDir, 0766)
file1 := addFile(t, sourceDir, "dir", "file")
file2 := addFile(t, sourceDir, "file")
destinationDir := path.Join(tmp, "destination")
err = MoveDirectory(sourceDir, destinationDir, false)
err := MoveDirectory(sourceDir, destinationDir, false)
require.NoError(t, err)
assert.NoFileExists(t, file1, "source dir contents should be moved")
assert.NoFileExists(t, file2, "source dir contents should be moved")

View File

@@ -15,8 +15,7 @@ func createService(t *testing.T) *Service {
require.NoError(t, err, "NewService should not fail")
t.Cleanup(func() {
err := os.RemoveAll(dataStorePath)
require.NoError(t, err)
os.RemoveAll(dataStorePath)
})
return service

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"golang.org/x/mod/semver"
@@ -28,8 +27,11 @@ func FilterDirForEntryFile(dirEntries []DirEntry, entryFile string) []DirEntry {
for _, dirEntry := range dirEntries {
match := false
if dirEntry.IsFile {
if slices.Contains(filters, dirEntry.Name) {
match = true
for _, filter := range filters {
if filter == dirEntry.Name {
match = true
break
}
}
} else {
for _, filter := range filters {
@@ -165,21 +167,3 @@ func DecodeDirEntries(dirEntries []DirEntry) error {
return nil
}
// GetDirEntriesByFilenames returns the dir entries that are files and match the provided filenames
func GetDirEntriesByFilenames(dirEntries []DirEntry, names []string) []DirEntry {
var filteredDirEntries []DirEntry
for _, dirEntry := range dirEntries {
if !dirEntry.IsFile {
continue
}
for _, name := range names {
if dirEntry.Name == name {
filteredDirEntries = append(filteredDirEntries, dirEntry)
}
}
}
return filteredDirEntries
}

View File

@@ -30,20 +30,6 @@ func MultiFilterDirForPerDevConfigs(dirEntries []DirEntry, configPath string, mu
return deduplicate(filteredDirEntries), envFiles
}
// MultiFilterDirForPerDevConfigsWithDefaults filers the given dirEntries with multiple filter args, returns the merged entries for the given device
// and always includes the defaultFilenames
func MultiFilterDirForPerDevConfigsWithDefaults(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs, defaultFilenames []string) ([]DirEntry, []string) {
filteredDirEntries, envFiles := MultiFilterDirForPerDevConfigs(dirEntries, configPath, multiFilterArgs)
// Add files that should always be included
// e.g. entrypoint files
defaultDirEntries := GetDirEntriesByFilenames(dirEntries, defaultFilenames)
filteredDirEntries = append(filteredDirEntries, defaultDirEntries...)
return deduplicate(filteredDirEntries), envFiles
}
func deduplicate(dirEntries []DirEntry) []DirEntry {
var deduplicatedDirEntries []DirEntry

View File

@@ -49,11 +49,8 @@ func TestMultiFilterDirForPerDevConfigs(t *testing.T) {
f(
baseDirEntries,
"configs",
MultiFilterArgs{
{"file1", portainer.PerDevConfigsTypeFile},
{"folder1", portainer.PerDevConfigsTypeDir},
},
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[5], baseDirEntries[6]},
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
)
// Filter file1 and file2
@@ -79,106 +76,6 @@ func TestMultiFilterDirForPerDevConfigs(t *testing.T) {
)
}
func TestMultiFilterDirForPerDevConfigsWithDefaults(t *testing.T) {
f := func(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs, defaultFilenames []string, wantDirEntries []DirEntry) {
t.Helper()
dirEntries, _ = MultiFilterDirForPerDevConfigsWithDefaults(dirEntries, configPath, multiFilterArgs, defaultFilenames)
require.Equal(t, wantDirEntries, dirEntries)
}
baseDirEntries := []DirEntry{
{".env", "", true, 420},
{"docker-compose.yaml", "", true, 420},
{"configs", "", false, 420},
{"configs/file1.conf", "", true, 420},
{"configs/file2.conf", "", true, 420},
{"configs/folder1", "", false, 420},
{"configs/folder1/config1", "", true, 420},
{"configs/folder2", "", false, 420},
{"configs/folder2/config2", "", true, 420},
{"configs/docker-compose-2.yaml", "", true, 420},
{"configs/folder2/docker-compose-3.yaml", "", true, 420},
}
// Filter file1
f(
baseDirEntries,
"configs",
MultiFilterArgs{{"file1", portainer.PerDevConfigsTypeFile}},
nil,
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3]},
)
// Filter folder1
f(
baseDirEntries,
"configs",
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
nil,
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
)
// Filter file1 and folder1
f(
baseDirEntries,
"configs",
MultiFilterArgs{
{"file1", portainer.PerDevConfigsTypeFile},
{"folder1", portainer.PerDevConfigsTypeDir},
},
nil,
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[5], baseDirEntries[6]},
)
// Filter file1 and file2
f(
baseDirEntries,
"configs",
MultiFilterArgs{
{"file1", portainer.PerDevConfigsTypeFile},
{"file2", portainer.PerDevConfigsTypeFile},
},
nil,
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[4]},
)
// Filter folder1 and folder2
f(
baseDirEntries,
"configs",
MultiFilterArgs{
{"folder1", portainer.PerDevConfigsTypeDir},
{"folder2", portainer.PerDevConfigsTypeDir},
},
nil,
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6], baseDirEntries[7], baseDirEntries[8], baseDirEntries[10]},
)
// Filter file1 and folder1 and docker-compose-2.yaml
f(
baseDirEntries,
"configs",
MultiFilterArgs{
{"file1", portainer.PerDevConfigsTypeFile},
{"folder1", portainer.PerDevConfigsTypeDir},
},
[]string{"configs/docker-compose-2.yaml"},
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[5], baseDirEntries[6], baseDirEntries[9]},
)
// Filter file1 and docker-compose-3.yaml
f(
baseDirEntries,
"configs",
MultiFilterArgs{
{"file1", portainer.PerDevConfigsTypeFile},
},
[]string{"configs/folder2/docker-compose-3.yaml"},
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[10]},
)
}
func TestMultiFilterDirForPerDevConfigsEnvFiles(t *testing.T) {
f := func(dirEntries []DirEntry, configPath string, multiFilterArgs MultiFilterArgs, wantEnvFiles []string) {
t.Helper()

View File

@@ -5,7 +5,6 @@ import (
"path/filepath"
"github.com/pkg/errors"
"github.com/portainer/portainer/api/logs"
)
// WriteToFile creates a file in the filesystem storage
@@ -18,7 +17,7 @@ func WriteToFile(dst string, content []byte) error {
if err != nil {
return errors.Wrapf(err, "failed to open a file %q", dst)
}
defer logs.CloseAndLogErr(file)
defer file.Close()
_, err = file.Write(content)
return errors.Wrapf(err, "failed to write a file %q", dst)

View File

@@ -13,8 +13,6 @@ import (
"github.com/portainer/portainer/api/archive"
"github.com/portainer/portainer/api/crypto"
gittypes "github.com/portainer/portainer/api/git/types"
"github.com/portainer/portainer/api/logs"
"github.com/rs/zerolog/log"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/pkg/errors"
@@ -78,13 +76,10 @@ func (a *azureClient) download(ctx context.Context, destination string, opt clon
if err != nil {
return errors.Wrap(err, "failed to download a zip file from Azure DevOps")
}
defer func() {
if err := os.Remove(zipFilepath); err != nil {
log.Warn().Err(err).Msg("failed to remove temporary zip file")
}
}()
defer os.Remove(zipFilepath)
if err := archive.UnzipFile(zipFilepath, destination); err != nil {
err = archive.UnzipFile(zipFilepath, destination)
if err != nil {
return errors.Wrap(err, "failed to unzip file")
}
@@ -107,7 +102,7 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
return "", errors.WithMessage(err, "failed to create temp file")
}
defer logs.CloseAndLogErr(zipFile)
defer zipFile.Close()
req, err := http.NewRequestWithContext(ctx, "GET", downloadUrl, nil)
if opt.username != "" || opt.password != "" {
@@ -128,17 +123,14 @@ func (a *azureClient) downloadZipFromAzureDevOps(ctx context.Context, opt cloneO
return "", errors.WithMessage(err, "failed to make an HTTP request")
}
defer func() {
if err := res.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to download zip with a status \"%v\"", res.Status)
}
if _, err := io.Copy(zipFile, res.Body); err != nil {
_, err = io.Copy(zipFile, res.Body)
if err != nil {
return "", errors.WithMessage(err, "failed to save HTTP response to a file")
}
@@ -183,11 +175,7 @@ func (a *azureClient) getRootItem(ctx context.Context, opt fetchOption) (*azureI
if err != nil {
return nil, errors.WithMessage(err, "failed to make an HTTP request")
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, checkAzureStatusCode(fmt.Errorf("failed to get repository root item with a status \"%v\"", resp.Status), resp.StatusCode)
@@ -377,12 +365,12 @@ const (
)
func formatReferenceName(name string) string {
if after, ok := strings.CutPrefix(name, branchPrefix); ok {
return after
if strings.HasPrefix(name, branchPrefix) {
return strings.TrimPrefix(name, branchPrefix)
}
if after, ok := strings.CutPrefix(name, tagPrefix); ok {
return after
if strings.HasPrefix(name, tagPrefix) {
return strings.TrimPrefix(name, tagPrefix)
}
return name
@@ -429,11 +417,7 @@ func (a *azureClient) listRefs(ctx context.Context, opt baseOption) ([]string, e
if err != nil {
return nil, errors.WithMessage(err, "failed to make an HTTP request")
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, checkAzureStatusCode(fmt.Errorf("failed to list refs with a status \"%v\"", resp.Status), resp.StatusCode)
@@ -493,11 +477,7 @@ func (a *azureClient) listFiles(ctx context.Context, opt fetchOption) ([]string,
if err != nil {
return nil, errors.WithMessage(err, "failed to make an HTTP request")
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to list tree url with a status \"%v\"", resp.Status)

View File

@@ -139,12 +139,8 @@ func TestService_ListRefs_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go func() {
_, _ = service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
}()
_, err := service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
require.NoError(t, err)
go service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListRefs(privateAzureRepoURL, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
time.Sleep(2 * time.Second)
}
@@ -157,7 +153,6 @@ func TestService_ListFiles_Azure(t *testing.T) {
err error
matchedCount int
}
service := newService(context.TODO(), 0, 0)
accessToken := getRequiredValue(t, "AZURE_DEVOPS_PAT")
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
@@ -294,7 +289,6 @@ func TestService_ListFiles_Azure(t *testing.T) {
tt.extensions,
false,
)
if tt.expect.shouldFail {
require.Error(t, err)
if tt.expect.err != nil {
@@ -317,21 +311,18 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
username := getRequiredValue(t, "AZURE_DEVOPS_USERNAME")
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go func() {
_, _ = service.ListFiles(
privateAzureRepoURL,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
}()
_, err := service.ListFiles(
go service.ListFiles(
privateAzureRepoURL,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
service.ListFiles(
privateAzureRepoURL,
"refs/heads/main",
username,
@@ -342,7 +333,6 @@ func TestService_ListFiles_Azure_Concurrently(t *testing.T) {
[]string{},
false,
)
require.NoError(t, err)
time.Sleep(2 * time.Second)
}
@@ -352,7 +342,6 @@ func getRequiredValue(t *testing.T, name string) string {
if !ok {
t.Fatalf("can't find required env var \"%s\"", name)
}
return value
}

View File

@@ -333,12 +333,13 @@ func Test_azureDownloader_latestCommitID(t *testing.T) {
]
}`
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(response))
w.Write([]byte(response))
}))
defer server.Close()
a := &azureClient{baseUrl: server.URL}
a := &azureClient{
baseUrl: server.URL,
}
tests := []struct {
name string
@@ -420,7 +421,7 @@ func Test_cloneRepository_azure(t *testing.T) {
git := &testRepoManager{}
s := &Service{azure: azure, git: git}
err := s.cloneRepository("", cloneOption{
s.cloneRepository("", cloneOption{
fetchOption: fetchOption{
baseOption: baseOption{
@@ -429,7 +430,6 @@ func Test_cloneRepository_azure(t *testing.T) {
},
depth: 1,
})
require.NoError(t, err)
// if azure API is called, git isn't and vice versa
assert.Equal(t, tt.called, azure.called)

View File

@@ -82,12 +82,8 @@ func TestService_ListRefs_Github_Concurrently(t *testing.T) {
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
repositoryUrl := privateGitRepoURL
go func() {
_, _ = service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
}()
_, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
require.NoError(t, err)
go service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
time.Sleep(2 * time.Second)
}
@@ -259,21 +255,18 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME")
service := newService(context.TODO(), repositoryCacheSize, 200*time.Millisecond)
go func() {
_, _ = service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
}()
_, err := service.ListFiles(
go service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
accessToken,
gittypes.GitCredentialAuthType_Basic,
false,
false,
[]string{},
false,
)
service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
@@ -284,7 +277,6 @@ func TestService_ListFiles_Github_Concurrently(t *testing.T) {
[]string{},
false,
)
require.NoError(t, err)
time.Sleep(2 * time.Second)
}
@@ -297,10 +289,8 @@ func TestService_purgeCache_Github(t *testing.T) {
username := getRequiredValue(t, "GITHUB_USERNAME")
service := NewService(context.TODO())
_, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
require.NoError(t, err)
_, err = service.ListFiles(
service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
@@ -311,7 +301,6 @@ func TestService_purgeCache_Github(t *testing.T) {
[]string{},
false,
)
require.NoError(t, err)
assert.Equal(t, 1, service.repoRefCache.Len())
assert.Equal(t, 1, service.repoFileCache.Len())
@@ -331,9 +320,8 @@ func TestService_purgeCacheByTTL_Github(t *testing.T) {
// 40*timeout is designed for giving enough time for ListRefs and ListFiles to cache the result
service := newService(context.TODO(), 2, 40*timeout)
_, err := service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
require.NoError(t, err)
_, err = service.ListFiles(
service.ListRefs(repositoryUrl, username, accessToken, gittypes.GitCredentialAuthType_Basic, false, false)
service.ListFiles(
repositoryUrl,
"refs/heads/main",
username,
@@ -344,7 +332,6 @@ func TestService_purgeCacheByTTL_Github(t *testing.T) {
[]string{},
false,
)
require.NoError(t, err)
assert.Equal(t, 1, service.repoRefCache.Len())
assert.Equal(t, 1, service.repoFileCache.Len())

View File

@@ -7,7 +7,6 @@ import (
"net/http"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
)
@@ -35,11 +34,7 @@ func (service *Service) Authorization(configuration portainer.OpenAMTConfigurati
if err != nil {
return "", err
}
defer func() {
if err := response.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer response.Body.Close()
responseBody, readErr := io.ReadAll(response.Body)
if readErr != nil {
@@ -52,7 +47,8 @@ func (service *Service) Authorization(configuration portainer.OpenAMTConfigurati
}
var token authenticationResponse
if err := json.Unmarshal(responseBody, &token); err != nil {
err = json.Unmarshal(responseBody, &token)
if err != nil {
return "", err
}

View File

@@ -10,7 +10,6 @@ import (
"strings"
portainer "github.com/portainer/portainer/api"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
)
@@ -130,11 +129,7 @@ func (service *Service) getCIRACertificate(configuration portainer.OpenAMTConfig
if err != nil {
return "", err
}
defer func() {
if err := response.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", fmt.Errorf("unexpected status code %s", response.Status)

View File

@@ -10,7 +10,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/crypto"
"github.com/rs/zerolog/log"
"github.com/segmentio/encoding/json"
"golang.org/x/sync/errgroup"
@@ -101,11 +100,7 @@ func (service *Service) executeSaveRequest(method string, url string, token stri
if err != nil {
return nil, err
}
defer func() {
if err := response.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer response.Body.Close()
responseBody, readErr := io.ReadAll(response.Body)
if readErr != nil {
@@ -117,7 +112,6 @@ func (service *Service) executeSaveRequest(method string, url string, token stri
if errorResponse != nil {
return nil, errorResponse
}
return nil, fmt.Errorf("unexpected status code %s", response.Status)
}
@@ -137,11 +131,7 @@ func (service *Service) executeGetRequest(url string, token string) ([]byte, err
if err != nil {
return nil, err
}
defer func() {
if err := response.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer response.Body.Close()
responseBody, readErr := io.ReadAll(response.Body)
if readErr != nil {

View File

@@ -55,11 +55,7 @@ func (client *HTTPClient) ExecuteAzureAuthenticationRequest(credentials *portain
if err != nil {
return nil, err
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New("invalid Azure credentials")
@@ -90,11 +86,7 @@ func Get(url string, timeout int) ([]byte, error) {
if err != nil {
return nil, err
}
defer func() {
if err := response.Body.Close(); err != nil {
log.Warn().Err(err).Msg("failed to close response body")
}
}()
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
log.Error().Int("status_code", response.StatusCode).Msg("unexpected status code")
@@ -146,11 +138,8 @@ func pingOperation(client *http.Client, target string) (bool, error) {
return false, 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()
agentOnDockerEnvironment := resp.Header.Get(portainer.PortainerAgentHeader) != ""

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