Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45106ec39c | |||
| 21937dfe60 | |||
| 90946ceca5 | |||
| 9cc3243166 | |||
| 1f20add37f | |||
| 60733427e6 | |||
| 3f451830cb | |||
| 9f0facc0f3 | |||
| a622122486 | |||
| 12fdc45ee5 | |||
| abf3d1450d | |||
| 1ae795d508 | |||
| 6f9ddd47de | |||
| 9507cf9d8b | |||
| 76e4054215 | |||
| 0a3e13915c | |||
| dbd6e49e5f | |||
| abad58a370 | |||
| 4eb1c7b11f | |||
| 3afedce570 | |||
| a7b6db72a5 | |||
| 9c79d6dc7d | |||
| 11f612a501 | |||
| cb8d8fcfd6 | |||
| 22bb1e604d | |||
| 970b135261 | |||
| a69470ec08 | |||
| ea6f1c97f5 | |||
| 6d058987f3 | |||
| 6998f05855 | |||
| 94d01c58fc | |||
| d98eb77067 | |||
| 941e86563a | |||
| f72d6b97d3 | |||
| 32926aa8bf | |||
| 1849c61c38 | |||
| fd6d74602c | |||
| 74b1dd04d1 | |||
| 7450501b7a |
+8
-2
@@ -17,7 +17,7 @@ plugins:
|
||||
- import
|
||||
|
||||
parserOptions:
|
||||
ecmaVersion: 2018
|
||||
ecmaVersion: latest
|
||||
sourceType: module
|
||||
project: './tsconfig.json'
|
||||
ecmaFeatures:
|
||||
@@ -114,7 +114,13 @@ 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/label-has-associated-control':
|
||||
- error
|
||||
- assert: either
|
||||
controlComponents:
|
||||
- Input
|
||||
- Checkbox
|
||||
'jsx-a11y/control-has-associated-label': off
|
||||
'react/function-component-definition': ['error', { 'namedComponents': 'function-declaration' }]
|
||||
'react/jsx-no-bind': off
|
||||
'no-await-in-loop': 'off'
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
cd $(dirname -- "$0") && yarn lint-staged
|
||||
cd $(dirname -- "$0") && pnpm lint-staged
|
||||
+1
-1
@@ -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, yarn, and Golang installed in the correct versions.
|
||||
Ensure you have Docker, Node.js, pnpm, and Golang installed in the correct versions.
|
||||
|
||||
Install dependencies:
|
||||
|
||||
|
||||
@@ -26,7 +26,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) && yarn build --config $(WEBPACK_CONFIG)
|
||||
export NODE_ENV=$(ENV) && pnpm run build --config $(WEBPACK_CONFIG)
|
||||
|
||||
build-server: init-dist ## Build the server binary
|
||||
./build/build_binary.sh "$(PLATFORM)" "$(ARCH)"
|
||||
@@ -35,7 +35,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
|
||||
yarn storybook:build
|
||||
pnpm run storybook:build
|
||||
|
||||
devops: clean deps build-client ## Build the everything target specifically for CI
|
||||
echo "Building the devops binary..."
|
||||
@@ -49,7 +49,7 @@ server-deps: init-dist ## Download dependant server binaries
|
||||
@./build/download_binaries.sh $(PLATFORM) $(ARCH)
|
||||
|
||||
client-deps: ## Install client dependencies
|
||||
yarn
|
||||
pnpm install
|
||||
|
||||
tidy: ## Tidy up the go.mod file
|
||||
@go mod tidy
|
||||
@@ -67,7 +67,7 @@ clean: ## Remove all build and download artifacts
|
||||
test: test-server test-client ## Run all tests
|
||||
|
||||
test-client: ## Run client tests
|
||||
yarn test $(ARGS) --coverage
|
||||
pnpm run 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 ./...
|
||||
@@ -79,7 +79,7 @@ dev: ## Run both the client and server in development mode
|
||||
make dev-client
|
||||
|
||||
dev-client: ## Run the client in development mode
|
||||
yarn dev
|
||||
pnpm run dev
|
||||
|
||||
dev-server: build-server ## Run the server in development mode
|
||||
@./dev/run_container.sh
|
||||
@@ -93,7 +93,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
|
||||
yarn format
|
||||
pnpm run format
|
||||
|
||||
format-server: ## Format server code
|
||||
go fmt ./...
|
||||
@@ -103,9 +103,9 @@ format-server: ## Format server code
|
||||
lint: lint-client lint-server ## Lint all code
|
||||
|
||||
lint-client: ## Lint client code
|
||||
yarn lint
|
||||
pnpm run lint
|
||||
|
||||
lint-server: ## Lint server code
|
||||
lint-server: tidy ## Lint server code
|
||||
golangci-lint run --timeout=10m -c .golangci.yaml
|
||||
|
||||
|
||||
@@ -118,11 +118,12 @@ 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
|
||||
yarn swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
|
||||
yarn swagger-cli validate dist/docs/openapi.yaml
|
||||
pnpm swagger2openapi --warnOnly dist/docs/swagger.yaml -o dist/docs/openapi.yaml
|
||||
pnpm swagger-cli validate dist/docs/openapi.yaml
|
||||
|
||||
##@ Helpers
|
||||
.PHONY: help
|
||||
|
||||
@@ -630,7 +630,7 @@ func main() {
|
||||
Str("build_number", build.BuildNumber).
|
||||
Str("image_tag", build.ImageTag).
|
||||
Str("nodejs_version", build.NodejsVersion).
|
||||
Str("yarn_version", build.YarnVersion).
|
||||
Str("pnpm_version", build.PnpmVersion).
|
||||
Str("webpack_version", build.WebpackVersion).
|
||||
Str("go_version", build.GoVersion).
|
||||
Msg("starting Portainer")
|
||||
|
||||
@@ -136,10 +136,8 @@ func (connection *DbConnection) NeedsEncryptionMigration() (bool, error) {
|
||||
func (connection *DbConnection) Open() error {
|
||||
log.Info().Str("filename", connection.GetDatabaseFileName()).Msg("loading PortainerDB")
|
||||
|
||||
// Now we open the db
|
||||
databasePath := connection.GetDatabaseFilePath()
|
||||
|
||||
db, err := bolt.Open(databasePath, 0600, connection.boltOptions())
|
||||
db, err := bolt.Open(databasePath, 0600, connection.boltOptions(connection.Compact))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -152,6 +150,15 @@ func (connection *DbConnection) Open() error {
|
||||
log.Info().Msg("compacting database")
|
||||
if err := connection.compact(); err != nil {
|
||||
log.Error().Err(err).Msg("failed to compact database")
|
||||
|
||||
// Close the read-only database and re-open in read-write mode
|
||||
if err := connection.Close(); err != nil {
|
||||
log.Warn().Err(err).Msg("failure to close the database after failed compaction")
|
||||
}
|
||||
|
||||
connection.Compact = false
|
||||
|
||||
return connection.Open()
|
||||
} else {
|
||||
log.Info().Msg("database compaction completed")
|
||||
}
|
||||
@@ -424,9 +431,14 @@ func (connection *DbConnection) RestoreMetadata(s map[string]any) error {
|
||||
}
|
||||
|
||||
// compact attempts to compact the database and replace it iff it succeeds
|
||||
func (connection *DbConnection) compact() error {
|
||||
func (connection *DbConnection) compact() (err error) {
|
||||
compactedPath := connection.GetDatabaseFilePath() + compactedSuffix
|
||||
compactedDB, err := bolt.Open(compactedPath, 0o600, connection.boltOptions())
|
||||
|
||||
if err := os.Remove(compactedPath); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("failure to remove an existing compacted database: %w", err)
|
||||
}
|
||||
|
||||
compactedDB, err := bolt.Open(compactedPath, 0o600, connection.boltOptions(false))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failure to create the compacted database: %w", err)
|
||||
}
|
||||
@@ -453,11 +465,12 @@ func (connection *DbConnection) compact() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (connection *DbConnection) boltOptions() *bolt.Options {
|
||||
func (connection *DbConnection) boltOptions(readOnly bool) *bolt.Options {
|
||||
return &bolt.Options{
|
||||
Timeout: 1 * time.Second,
|
||||
InitialMmapSize: connection.InitialMmapSize,
|
||||
FreelistType: bolt.FreelistMapType,
|
||||
NoFreelistSync: true,
|
||||
ReadOnly: readOnly,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.etcd.io/bbolt"
|
||||
@@ -123,10 +125,7 @@ func Test_NeedsEncryptionMigration(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDBCompaction(t *testing.T) {
|
||||
db := &DbConnection{
|
||||
Path: t.TempDir(),
|
||||
Compact: true,
|
||||
}
|
||||
db := &DbConnection{Path: t.TempDir()}
|
||||
|
||||
err := db.Open()
|
||||
require.NoError(t, err)
|
||||
@@ -147,6 +146,7 @@ func TestDBCompaction(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Reopen the DB to trigger compaction
|
||||
db.Compact = true
|
||||
err = db.Open()
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -168,10 +168,14 @@ func TestDBCompaction(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Failures
|
||||
|
||||
err = os.Mkdir(db.GetDatabaseFilePath()+compactedSuffix, 0o755)
|
||||
compactedPath := db.GetDatabaseFilePath() + compactedSuffix
|
||||
err = os.Mkdir(compactedPath, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := os.Create(filesystem.JoinPaths(compactedPath, "somefile"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, f.Close())
|
||||
|
||||
err = db.Open()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -615,7 +615,7 @@
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.33.2",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.33.7",
|
||||
"LDAPSettings": {
|
||||
"AnonymousMode": true,
|
||||
"AutoCreateUsers": true,
|
||||
@@ -944,7 +944,7 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.33.2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.33.7\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
},
|
||||
"webhooks": null
|
||||
}
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -3,7 +3,9 @@ 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) {
|
||||
@@ -13,3 +15,29 @@ 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)
|
||||
}
|
||||
|
||||
@@ -167,3 +167,21 @@ 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
|
||||
}
|
||||
|
||||
@@ -30,6 +30,20 @@ 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
|
||||
|
||||
|
||||
@@ -49,8 +49,11 @@ func TestMultiFilterDirForPerDevConfigs(t *testing.T) {
|
||||
f(
|
||||
baseDirEntries,
|
||||
"configs",
|
||||
MultiFilterArgs{{"folder1", portainer.PerDevConfigsTypeDir}},
|
||||
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[5], baseDirEntries[6]},
|
||||
MultiFilterArgs{
|
||||
{"file1", portainer.PerDevConfigsTypeFile},
|
||||
{"folder1", portainer.PerDevConfigsTypeDir},
|
||||
},
|
||||
[]DirEntry{baseDirEntries[0], baseDirEntries[1], baseDirEntries[2], baseDirEntries[3], baseDirEntries[5], baseDirEntries[6]},
|
||||
)
|
||||
|
||||
// Filter file1 and file2
|
||||
@@ -76,6 +79,106 @@ 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()
|
||||
|
||||
@@ -77,8 +77,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
var edgeGroup *portainer.EdgeGroup
|
||||
|
||||
var shadowEdgeGroup shadowedEdgeGroup
|
||||
err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeGroups, err := tx.EdgeGroup().ReadAll()
|
||||
if err != nil {
|
||||
@@ -91,7 +90,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
edgeGroup = &portainer.EdgeGroup{
|
||||
edgeGroup := &portainer.EdgeGroup{
|
||||
Name: payload.Name,
|
||||
Dynamic: payload.Dynamic,
|
||||
TagIDs: []portainer.TagID{},
|
||||
@@ -108,8 +107,10 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.InternalServerError("Unable to persist the Edge group inside the database", err)
|
||||
}
|
||||
|
||||
shadowEdgeGroup = shadowedEdgeGroup{EdgeGroup: *edgeGroup}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return txResponse(w, shadowedEdgeGroup{EdgeGroup: *edgeGroup}, err)
|
||||
return txResponse(w, shadowEdgeGroup, err)
|
||||
}
|
||||
|
||||
@@ -60,3 +60,22 @@ func TestEdgeGroupCreateHandler(t *testing.T) {
|
||||
|
||||
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
|
||||
}
|
||||
|
||||
func TestEdgeGroupCreatePanic(t *testing.T) {
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
|
||||
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||
handler.DataStore = store
|
||||
|
||||
err := store.EdgeGroup().Create(&portainer.EdgeGroup{ID: 1, Name: "New Edge Group"})
|
||||
require.NoError(t, err)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost,
|
||||
"/edge_groups",
|
||||
strings.NewReader(`{"Name": "New Edge Group", "Endpoints": [1, 2, 3]}`),
|
||||
)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusBadRequest, rr.Result().StatusCode)
|
||||
}
|
||||
|
||||
@@ -28,15 +28,21 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.BadRequest("Invalid Edge group identifier route variable", err)
|
||||
}
|
||||
|
||||
var edgeGroup *portainer.EdgeGroup
|
||||
var shadowEdgeGroup shadowedEdgeGroup
|
||||
err = handler.DataStore.ViewTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeGroup, err = getEdgeGroup(tx, portainer.EdgeGroupID(edgeGroupID))
|
||||
return err
|
||||
edgeGroup, err := getEdgeGroup(tx, portainer.EdgeGroupID(edgeGroupID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
edgeGroup.Endpoints = edgeGroup.EndpointIDs.ToSlice()
|
||||
|
||||
shadowEdgeGroup = shadowedEdgeGroup{EdgeGroup: *edgeGroup}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
edgeGroup.Endpoints = edgeGroup.EndpointIDs.ToSlice()
|
||||
|
||||
return txResponse(w, shadowedEdgeGroup{EdgeGroup: *edgeGroup}, err)
|
||||
return txResponse(w, shadowEdgeGroup, err)
|
||||
}
|
||||
|
||||
func getEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
|
||||
|
||||
@@ -174,3 +174,16 @@ func TestDynamicEdgeGroupInspectHandler(t *testing.T) {
|
||||
|
||||
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
|
||||
}
|
||||
|
||||
func TestEdgeGroupInspectPanic(t *testing.T) {
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
|
||||
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||
handler.DataStore = store
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/edge_groups/1", nil)
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusNotFound, rr.Result().StatusCode)
|
||||
}
|
||||
|
||||
@@ -56,9 +56,9 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
var edgeGroup *portainer.EdgeGroup
|
||||
var shadowEdgeGroup shadowedEdgeGroup
|
||||
err = handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
edgeGroup, err = tx.EdgeGroup().Read(portainer.EdgeGroupID(edgeGroupID))
|
||||
edgeGroup, err := tx.EdgeGroup().Read(portainer.EdgeGroupID(edgeGroupID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find an Edge group with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
@@ -155,10 +155,12 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
}
|
||||
|
||||
shadowEdgeGroup = shadowedEdgeGroup{EdgeGroup: *edgeGroup}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return txResponse(w, shadowedEdgeGroup{EdgeGroup: *edgeGroup}, err)
|
||||
return txResponse(w, shadowEdgeGroup, err)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
|
||||
|
||||
@@ -68,3 +68,16 @@ func TestEdgeGroupUpdateHandler(t *testing.T) {
|
||||
|
||||
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
|
||||
}
|
||||
|
||||
func TestEdgeGroupUpdatePanic(t *testing.T) {
|
||||
_, store := datastore.MustNewTestStore(t, true, true)
|
||||
|
||||
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||
handler.DataStore = store
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPut, "/edge_groups/1", strings.NewReader("{}"))
|
||||
|
||||
handler.ServeHTTP(rr, req)
|
||||
require.Equal(t, http.StatusNotFound, rr.Result().StatusCode)
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
|
||||
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment ID: %d", err, payload.EndpointID))
|
||||
}
|
||||
|
||||
var stack *portainer.EdgeStack
|
||||
|
||||
@@ -42,17 +42,17 @@ func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
|
||||
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
edgeJobID, err := request.RetrieveNumericRouteVariableValue(r, "jobID")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid edge job identifier route variable", fmt.Errorf("invalid Edge job route variable: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.BadRequest("Invalid edge job identifier route variable", fmt.Errorf("invalid Edge job route variable: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
var payload logsPayload
|
||||
if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", fmt.Errorf("invalid Edge job request payload: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.BadRequest("Invalid request payload", fmt.Errorf("invalid Edge job request payload: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
if err := handler.DataStore.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
@@ -60,11 +60,11 @@ func (handler *Handler) endpointEdgeJobsLogs(w http.ResponseWriter, r *http.Requ
|
||||
}); err != nil {
|
||||
var httpErr *httperror.HandlerError
|
||||
if errors.As(err, &httpErr) {
|
||||
httpErr.Err = fmt.Errorf("edge polling error: %w. Environment name: %s", httpErr.Err, endpoint.Name)
|
||||
httpErr.Err = fmt.Errorf("edge polling error: %w. Environment ID: %d", httpErr.Err, endpoint.ID)
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", fmt.Errorf("edge polling error: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.InternalServerError("Unexpected error", fmt.Errorf("edge polling error: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
return response.JSON(w, nil)
|
||||
|
||||
@@ -40,18 +40,18 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
|
||||
}
|
||||
|
||||
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
|
||||
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.Forbidden("Permission denied to access environment", fmt.Errorf("unauthorized edge endpoint operation: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
edgeStackID, err := request.RetrieveNumericRouteVariableValue(r, "stackId")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid edge stack identifier route variable", fmt.Errorf("invalid Edge stack route variable: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.BadRequest("Invalid edge stack identifier route variable", fmt.Errorf("invalid Edge stack route variable: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
s, err, _ := edgeStackSingleFlightGroup.Do(strconv.Itoa(edgeStackID), func() (any, error) {
|
||||
edgeStack, err := handler.DataStore.EdgeStack().EdgeStack(portainer.EdgeStackID(edgeStackID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return nil, httperror.NotFound("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("unable to find the Edge stack from database: %w. Environment name: %s", err, endpoint.Name))
|
||||
return nil, httperror.NotFound("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("unable to find the Edge stack from database: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
return edgeStack, err
|
||||
@@ -62,7 +62,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("failed to find Edge stack from the database: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.InternalServerError("Unable to find an edge stack with the specified identifier inside the database", fmt.Errorf("failed to find Edge stack from the database: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
// WARNING: this variable must not be mutated
|
||||
@@ -71,7 +71,7 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
|
||||
fileName := edgeStack.EntryPoint
|
||||
if endpointutils.IsDockerEndpoint(endpoint) {
|
||||
if fileName == "" {
|
||||
return httperror.BadRequest("Docker is not supported by this stack", fmt.Errorf("no filename is provided for the Docker endpoint. Environment name: %s", endpoint.Name))
|
||||
return httperror.BadRequest("Docker is not supported by this stack", fmt.Errorf("no filename is provided for the Docker endpoint. Environment ID: %d", endpoint.ID))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,18 +84,18 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
|
||||
fileName = edgeStack.ManifestPath
|
||||
|
||||
if fileName == "" {
|
||||
return httperror.BadRequest("Kubernetes is not supported by this stack", fmt.Errorf("no filename is provided for the Kubernetes endpoint. Environment name: %s", endpoint.Name))
|
||||
return httperror.BadRequest("Kubernetes is not supported by this stack", fmt.Errorf("no filename is provided for the Kubernetes endpoint. Environment ID: %d", endpoint.ID))
|
||||
}
|
||||
}
|
||||
|
||||
dirEntries, err := filesystem.LoadDir(edgeStack.ProjectPath)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to load repository", fmt.Errorf("failed to load project directory: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.InternalServerError("Unable to load repository", fmt.Errorf("failed to load project directory: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
fileContent, err := filesystem.FilterDirForCompatibility(dirEntries, fileName, endpoint.Agent.Version)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("File not found", fmt.Errorf("unable to find file: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.InternalServerError("File not found", fmt.Errorf("unable to find file: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
dirEntries = filesystem.FilterDirForEntryFile(dirEntries, fileName)
|
||||
|
||||
@@ -97,13 +97,13 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
|
||||
firstConn := endpoint.LastCheckInDate == 0
|
||||
|
||||
if err := handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint); err != nil {
|
||||
return httperror.Forbidden("Permission denied to access environment. The device has not been trusted yet", fmt.Errorf("unauthorized Edge endpoint operation: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.Forbidden("Permission denied to access environment. The device has not been trusted yet", fmt.Errorf("unauthorized Edge endpoint operation: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
handler.DataStore.Endpoint().UpdateHeartbeat(endpoint.ID)
|
||||
|
||||
if err := handler.requestBouncer.TrustedEdgeEnvironmentAccess(handler.DataStore, endpoint); err != nil {
|
||||
return httperror.Forbidden("Permission denied to access environment. The device has not been trusted yet", fmt.Errorf("untrusted Edge environment access: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.Forbidden("Permission denied to access environment. The device has not been trusted yet", fmt.Errorf("untrusted Edge environment access: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
var statusResponse *endpointEdgeStatusInspectResponse
|
||||
@@ -113,11 +113,11 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
|
||||
}); err != nil {
|
||||
var httpErr *httperror.HandlerError
|
||||
if errors.As(err, &httpErr) {
|
||||
httpErr.Err = fmt.Errorf("edge polling error: %w. Environment name: %s", httpErr.Err, endpoint.Name)
|
||||
httpErr.Err = fmt.Errorf("edge polling error: %w. Environment ID: %d", httpErr.Err, endpoint.ID)
|
||||
return httpErr
|
||||
}
|
||||
|
||||
return httperror.InternalServerError("Unexpected error", fmt.Errorf("edge polling error: %w. Environment name: %s", err, endpoint.Name))
|
||||
return httperror.InternalServerError("Unexpected error", fmt.Errorf("edge polling error: %w. Environment ID: %d", err, endpoint.ID))
|
||||
}
|
||||
|
||||
return cacheResponse(w, endpoint.ID, *statusResponse)
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
// @produce json
|
||||
// @param id path int true "Environment(Endpoint) identifier"
|
||||
// @param excludeSnapshot query bool false "if true, the snapshot data won't be retrieved"
|
||||
// @param excludeSnapshotRaw query bool false "if true, the SnapshotRaw field won't be retrieved"
|
||||
// @success 200 {object} portainer.Endpoint "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 404 "Environment(Endpoint) not found"
|
||||
@@ -53,10 +52,9 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||
|
||||
excludeSnapshot, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshot", true)
|
||||
excludeRaw, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshotRaw", true)
|
||||
|
||||
if !excludeSnapshot {
|
||||
if err := handler.SnapshotService.FillSnapshotData(endpoint, !excludeRaw); err != nil {
|
||||
if err := handler.SnapshotService.FillSnapshotData(endpoint, false); err != nil {
|
||||
return httperror.InternalServerError("Unable to add snapshot data", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ const (
|
||||
// @param edgeDeviceUntrusted query bool false "if true, show only untrusted edge agents, if false show only trusted edge agents (relevant only for edge agents)"
|
||||
// @param edgeCheckInPassedSeconds query number false "if bigger then zero, show only edge agents that checked-in in the last provided seconds (relevant only for edge agents)"
|
||||
// @param excludeSnapshots query bool false "if true, the snapshot data won't be retrieved"
|
||||
// @param excludeSnapshotRaw query bool false "if true, the SnapshotRaw field won't be retrieved"
|
||||
// @param name query string false "will return only environments(endpoints) with this name"
|
||||
// @param edgeStackId query portainer.EdgeStackID false "will return the environements of the specified edge stack"
|
||||
// @param edgeStackStatus query string false "only applied when edgeStackId exists. Filter the returned environments based on their deployment status in the stack (not the environment status!)" Enum("Pending", "Ok", "Error", "Acknowledged", "Remove", "RemoteUpdateSuccess", "ImagesPulled")
|
||||
@@ -63,7 +62,6 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
|
||||
sortField, _ := request.RetrieveQueryParameter(r, "sort", true)
|
||||
sortOrder, _ := request.RetrieveQueryParameter(r, "order", true)
|
||||
excludeRaw, _ := request.RetrieveBooleanQueryParameter(r, "excludeSnapshotRaw", true)
|
||||
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().ReadAll()
|
||||
if err != nil {
|
||||
@@ -118,7 +116,7 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
endpointutils.UpdateEdgeEndpointHeartbeat(&paginatedEndpoints[idx], settings)
|
||||
|
||||
if !query.excludeSnapshots {
|
||||
if err := handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx], !excludeRaw); err != nil {
|
||||
if err := handler.SnapshotService.FillSnapshotData(&paginatedEndpoints[idx], false); err != nil {
|
||||
return httperror.InternalServerError("Unable to add snapshot data", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.33.2
|
||||
// @version 2.33.7
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -161,7 +161,13 @@ func (handler *Handler) startStack(
|
||||
return handler.StackDeployer.StartRemoteComposeStack(stack, endpoint, filteredRegistries)
|
||||
}
|
||||
|
||||
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, portainer.ComposeUpOptions{})
|
||||
options := portainer.ComposeUpOptions{
|
||||
ComposeOptions: portainer.ComposeOptions{
|
||||
Registries: filteredRegistries,
|
||||
},
|
||||
}
|
||||
|
||||
return handler.ComposeStackManager.Up(context.TODO(), stack, endpoint, options)
|
||||
case portainer.DockerSwarmStack:
|
||||
stack.Name = handler.SwarmStackManager.NormalizeStackName(stack.Name)
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ var prefixProxyFuncMap = map[string]func(*Transport, *http.Request, string) (*ht
|
||||
// ProxyDockerRequest intercepts a Docker API request and apply logic based
|
||||
// on the requested operation.
|
||||
func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||
// from : /v1.41/containers/{id}/json
|
||||
// from : /v1.44/containers/{id}/json
|
||||
// or : /containers/{id}/json
|
||||
// to : /containers/{id}/json
|
||||
unversionedPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
||||
|
||||
@@ -23,6 +23,11 @@ var allowedHeaders = map[string]struct{}{
|
||||
"X-Portainer-Volumename": {},
|
||||
"X-Registry-Auth": {},
|
||||
"X-Stream-Protocol-Version": {},
|
||||
// WebSocket headers those are required for kubectl exec/attach/port-forward operations
|
||||
"Sec-Websocket-Key": {},
|
||||
"Sec-Websocket-Version": {},
|
||||
"Sec-Websocket-Protocol": {},
|
||||
"Sec-Websocket-Extensions": {},
|
||||
}
|
||||
|
||||
// newSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
|
||||
@@ -12,6 +12,16 @@ import (
|
||||
utilexec "k8s.io/client-go/util/exec"
|
||||
)
|
||||
|
||||
var (
|
||||
channelProtocolList = []string{
|
||||
"v5.channel.k8s.io",
|
||||
"v4.channel.k8s.io",
|
||||
"v3.channel.k8s.io",
|
||||
"v2.channel.k8s.io",
|
||||
"channel.k8s.io",
|
||||
}
|
||||
)
|
||||
|
||||
// StartExecProcess will start an exec process inside a container located inside a pod inside a specific namespace
|
||||
// using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write
|
||||
// to the stdout parameter.
|
||||
@@ -45,10 +55,18 @@ func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namesp
|
||||
TTY: true,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
|
||||
exec, err := remotecommand.NewWebSocketExecutorForProtocols(
|
||||
config,
|
||||
"GET", // WebSocket uses GET for the upgrade request
|
||||
req.URL().String(),
|
||||
channelProtocolList...,
|
||||
)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
exec, err = remotecommand.NewSPDYExecutor(config, "POST", req.URL())
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = exec.StreamWithContext(context.TODO(), remotecommand.StreamOptions{
|
||||
|
||||
+1
-1
@@ -1782,7 +1782,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.33.2"
|
||||
APIVersion = "2.33.7"
|
||||
// Support annotation for the API version ("STS" for Short-Term Support or "LTS" for Long-Term Support)
|
||||
APIVersionSupport = "LTS"
|
||||
// Edition is what this edition of Portainer is called
|
||||
|
||||
@@ -55,12 +55,11 @@ func (d *stackDeployer) DeployRemoteComposeStack(
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.swarmStackManager.Login(registries, endpoint)
|
||||
defer d.swarmStackManager.Logout(endpoint)
|
||||
options := portainer.ComposeOptions{Registries: registries}
|
||||
|
||||
// --force-recreate doesn't pull updated images
|
||||
if forcePullImage {
|
||||
if err := d.composeStackManager.Pull(context.TODO(), stack, endpoint, portainer.ComposeOptions{}); err != nil {
|
||||
if err := d.composeStackManager.Pull(context.TODO(), stack, endpoint, options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,9 +117,7 @@ div.input-mask {
|
||||
.widget .widget-body .error {
|
||||
color: #ff0000;
|
||||
}
|
||||
.widget .widget-body button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.widget .widget-body div.alert {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NodeStatus, TaskState } from 'docker-types/generated/1.41';
|
||||
import { NodeStatus, TaskState } from 'docker-types/generated/1.44';
|
||||
import _ from 'lodash';
|
||||
|
||||
export function trimVersionTag(fullName: string) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Config } from 'docker-types/generated/1.41';
|
||||
import { Config } from 'docker-types/generated/1.44';
|
||||
|
||||
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
|
||||
import { PortainerResponse } from '@/react/docker/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ImageSummary } from 'docker-types/generated/1.41';
|
||||
import { ImageSummary } from 'docker-types/generated/1.44';
|
||||
|
||||
import { PortainerResponse } from '@/react/docker/types';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ImageInspect } from 'docker-types/generated/1.41';
|
||||
import { ImageInspect } from 'docker-types/generated/1.44';
|
||||
|
||||
type ImageInspectConfig = NonNullable<ImageInspect['Config']>;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IPAM, Network, NetworkContainer } from 'docker-types/generated/1.41';
|
||||
import { IPAM, Network, NetworkContainer } from 'docker-types/generated/1.44';
|
||||
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ObjectVersion,
|
||||
Platform,
|
||||
ResourceObject,
|
||||
} from 'docker-types/generated/1.41';
|
||||
} from 'docker-types/generated/1.44';
|
||||
|
||||
export class NodeViewModel {
|
||||
Model: Node;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Secret } from 'docker-types/generated/1.41';
|
||||
import { Secret } from 'docker-types/generated/1.44';
|
||||
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
import { PortainerResponse } from '@/react/docker/types';
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Service,
|
||||
ServiceSpec,
|
||||
TaskSpec,
|
||||
} from 'docker-types/generated/1.41';
|
||||
} from 'docker-types/generated/1.44';
|
||||
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
import { PortainerResponse } from '@/react/docker/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Task } from 'docker-types/generated/1.41';
|
||||
import { Task } from 'docker-types/generated/1.44';
|
||||
|
||||
import { DeepPick } from '@/types/deepPick';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Volume } from 'docker-types/generated/1.41';
|
||||
import { Volume } from 'docker-types/generated/1.44';
|
||||
|
||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||
import { IResource } from '@/react/docker/components/datatable/createOwnershipColumn';
|
||||
|
||||
@@ -720,7 +720,7 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
|
||||
$scope.onResetPorts = function (all = false) {
|
||||
$scope.$evalAsync(() => {
|
||||
$scope.formValues.ports = portsMappingUtils.toViewModel($scope.service.Model.Spec.EndpointSpec.Ports);
|
||||
$scope.formValues.ports = portsMappingUtils.toViewModel($scope.service.Model.Spec.EndpointSpec?.Ports);
|
||||
|
||||
$scope.cancelChanges($scope.service, all ? undefined : ['Ports']);
|
||||
});
|
||||
@@ -744,7 +744,7 @@ angular.module('portainer.docker').controller('ServiceController', [
|
||||
$scope.lastVersion = service.Version;
|
||||
}
|
||||
|
||||
$scope.formValues.ports = portsMappingUtils.toViewModel(service.Model.Spec.EndpointSpec.Ports);
|
||||
$scope.formValues.ports = portsMappingUtils.toViewModel(service.Model.Spec.EndpointSpec?.Ports);
|
||||
|
||||
transformResources(service);
|
||||
translateServiceArrays(service);
|
||||
|
||||
@@ -4,8 +4,4 @@ export const MaxDockerAPIVersionKey = 'maxDockerAPIVersion' as const;
|
||||
export type DockerAPIVersionType = number;
|
||||
|
||||
// this is the version we are using with the generated API types
|
||||
export const MAX_DOCKER_API_VERSION: DockerAPIVersionType = 1.41;
|
||||
|
||||
// https://docs.docker.com/engine/api/#api-version-matrix
|
||||
// Docker 26 = API 1.45
|
||||
export const LATEST_DOCKER_API_VERSION: DockerAPIVersionType = 1.45;
|
||||
export const MAX_DOCKER_API_VERSION: DockerAPIVersionType = 1.44;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SystemVersion } from 'docker-types/generated/1.41';
|
||||
import { SystemVersion } from 'docker-types/generated/1.44';
|
||||
import Axios, { InternalAxiosRequestConfig } from 'axios';
|
||||
import { setupCache, buildMemoryStorage } from 'axios-cache-interceptor';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Header, flexRender } from '@tanstack/react-table';
|
||||
import { Header, flexRender, ColumnMeta } from '@tanstack/react-table';
|
||||
|
||||
import { filterHOC } from './Filter';
|
||||
import { TableHeaderCell } from './TableHeaderCell';
|
||||
@@ -17,9 +17,9 @@ export function TableHeaderRow<D extends DefaultType = DefaultType>({
|
||||
<tr>
|
||||
{headers.map((header) => {
|
||||
const sortDirection = header.column.getIsSorted();
|
||||
const {
|
||||
meta: { className, width } = { className: '', width: undefined },
|
||||
} = header.column.columnDef;
|
||||
const { className, filter, width } = parseMeta(
|
||||
header.column.columnDef.meta
|
||||
);
|
||||
|
||||
return (
|
||||
<TableHeaderCell
|
||||
@@ -43,13 +43,9 @@ export function TableHeaderRow<D extends DefaultType = DefaultType>({
|
||||
renderFilter={
|
||||
header.column.getCanFilter()
|
||||
? () =>
|
||||
flexRender(
|
||||
header.column.columnDef.meta?.filter ||
|
||||
filterHOC('Filter'),
|
||||
{
|
||||
column: header.column,
|
||||
}
|
||||
)
|
||||
flexRender(filter, {
|
||||
column: header.column,
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
@@ -58,3 +54,28 @@ export function TableHeaderRow<D extends DefaultType = DefaultType>({
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function parseMeta<D extends DefaultType = DefaultType>(
|
||||
meta: ColumnMeta<D, unknown> | undefined
|
||||
) {
|
||||
if (!meta) {
|
||||
return {
|
||||
className: '',
|
||||
width: undefined,
|
||||
filter: filterHOC('Filter'),
|
||||
};
|
||||
}
|
||||
|
||||
const className =
|
||||
'className' in meta && typeof meta.className === 'string'
|
||||
? meta.className
|
||||
: undefined;
|
||||
const width =
|
||||
'width' in meta && typeof meta.width === 'string' ? meta.width : undefined;
|
||||
const filter =
|
||||
'filter' in meta && typeof meta.filter === 'function'
|
||||
? meta.filter
|
||||
: filterHOC('Filter');
|
||||
|
||||
return { className, width, filter };
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Cell, flexRender } from '@tanstack/react-table';
|
||||
import { Cell, ColumnMeta, flexRender } from '@tanstack/react-table';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { DefaultType } from './types';
|
||||
@@ -20,10 +20,18 @@ export function TableRow<D extends DefaultType = DefaultType>({
|
||||
onClick={onClick}
|
||||
>
|
||||
{cells.map((cell) => (
|
||||
<td key={cell.id} className={cell.column.columnDef.meta?.className}>
|
||||
<td key={cell.id} className={getClassName(cell.column.columnDef.meta)}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function getClassName<D extends DefaultType = DefaultType>(
|
||||
meta: ColumnMeta<D, unknown> | undefined
|
||||
) {
|
||||
return !!meta && 'className' in meta && typeof meta.className === 'string'
|
||||
? meta.className
|
||||
: '';
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@ export type FilesTableMeta = TableMeta<FileData> & {
|
||||
export function isFilesTableMeta(
|
||||
meta?: TableMeta<FileData>
|
||||
): meta is FilesTableMeta {
|
||||
return !!meta && meta.table === 'files';
|
||||
return !!meta && 'table' in meta && meta.table === 'files';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Config } from 'docker-types/generated/1.41';
|
||||
import { Config } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Config } from 'docker-types/generated/1.41';
|
||||
import { Config } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConfigSpec } from 'docker-types/generated/1.41';
|
||||
import { ConfigSpec } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PortMap } from 'docker-types/generated/1.41';
|
||||
import { PortMap } from 'docker-types/generated/1.44';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { PortMapping, Protocol, Values } from './PortsMappingField';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PortMap } from 'docker-types/generated/1.41';
|
||||
import { PortMap } from 'docker-types/generated/1.44';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { Protocol, Values } from './PortsMappingField';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { mixed } from 'yup';
|
||||
import { ContainerConfig } from 'docker-types/generated/1.41';
|
||||
import { ContainerConfig } from 'docker-types/generated/1.44';
|
||||
|
||||
import { AutomationTestingProps } from '@/types';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HostConfig } from 'docker-types/generated/1.41';
|
||||
import { HostConfig } from 'docker-types/generated/1.44';
|
||||
|
||||
import { commandArrayToString } from '@/docker/helpers/containers';
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { CreateContainerRequest } from '../types';
|
||||
|
||||
import { toRequest } from './toRequest';
|
||||
import { Values } from './types';
|
||||
|
||||
describe('toRequest', () => {
|
||||
const mockOldConfig: CreateContainerRequest = {
|
||||
Hostname: 'old-hostname',
|
||||
Domainname: 'old-domain',
|
||||
MacAddress: '02:42:ac:11:00:99',
|
||||
HostConfig: {
|
||||
NetworkMode: 'bridge',
|
||||
Dns: ['1.1.1.1'],
|
||||
ExtraHosts: [],
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
bridge: {
|
||||
Aliases: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockValues: Values = {
|
||||
networkMode: 'bridge',
|
||||
hostname: 'new-hostname',
|
||||
domain: 'new-domain',
|
||||
macAddress: '02:42:ac:11:00:88',
|
||||
ipv4Address: '172.17.0.5',
|
||||
ipv6Address: 'fe80::42:acff:fe11:5',
|
||||
primaryDns: '8.8.8.8',
|
||||
secondaryDns: '8.8.4.4',
|
||||
hostsFileEntries: ['host1:127.0.0.1'],
|
||||
container: '',
|
||||
};
|
||||
|
||||
it('should use MAC address from values, not from oldConfig', () => {
|
||||
const oldMacAddress = '02:42:ac:11:00:99';
|
||||
const macAddress = '02:42:ac:11:00:88';
|
||||
const result = toRequest(
|
||||
{ ...mockOldConfig, MacAddress: oldMacAddress },
|
||||
{ ...mockValues, macAddress },
|
||||
'container-123'
|
||||
);
|
||||
|
||||
expect(result.MacAddress).toBe(macAddress);
|
||||
expect(result.MacAddress).not.toBe(oldMacAddress);
|
||||
});
|
||||
|
||||
it('should allow empty MAC address when duplicating containers', () => {
|
||||
const valuesWithEmptyMac: Values = {
|
||||
...mockValues,
|
||||
macAddress: '', // Empty MAC from toViewModel
|
||||
};
|
||||
|
||||
const result = toRequest(
|
||||
mockOldConfig,
|
||||
valuesWithEmptyMac,
|
||||
'container-123'
|
||||
);
|
||||
|
||||
expect(result.MacAddress).toBe('');
|
||||
expect(result.MacAddress).not.toBe(mockOldConfig.MacAddress);
|
||||
});
|
||||
|
||||
it('should set other network properties from values', () => {
|
||||
const result = toRequest(mockOldConfig, mockValues, 'container-123');
|
||||
|
||||
expect(result.Hostname).toBe('new-hostname');
|
||||
expect(result.Domainname).toBe('new-domain');
|
||||
expect(result.HostConfig.NetworkMode).toBe('bridge');
|
||||
expect(result.HostConfig.Dns).toEqual(['8.8.8.8', '8.8.4.4']);
|
||||
expect(result.HostConfig.ExtraHosts).toEqual(['host1:127.0.0.1']);
|
||||
expect(result.NetworkingConfig.EndpointsConfig?.bridge.IPAMConfig).toEqual({
|
||||
IPv4Address: '172.17.0.5',
|
||||
IPv6Address: 'fe80::42:acff:fe11:5',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,11 @@ import { DockerNetwork } from '@/react/docker/networks/types';
|
||||
import { ContainerListViewModel } from '../../types';
|
||||
import { ContainerDetailsJSON } from '../../queries/useContainer';
|
||||
|
||||
import { getDefaultViewModel, getNetworkMode } from './toViewModel';
|
||||
import {
|
||||
getDefaultViewModel,
|
||||
getNetworkMode,
|
||||
toViewModel,
|
||||
} from './toViewModel';
|
||||
|
||||
describe('getDefaultViewModel', () => {
|
||||
it('should return the correct default view model for Windows', () => {
|
||||
@@ -145,3 +149,86 @@ describe('getNetworkMode', () => {
|
||||
expect(getNetworkMode(config, mockNetworks)).toEqual(['bridge']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toViewModel', () => {
|
||||
const mockNetworks: Array<DockerNetwork> = [
|
||||
{
|
||||
Name: 'bridge',
|
||||
Id: 'bridge-id',
|
||||
Driver: 'bridge',
|
||||
Scope: 'local',
|
||||
Attachable: false,
|
||||
Internal: false,
|
||||
IPAM: { Config: [], Driver: '', Options: {} },
|
||||
Options: {},
|
||||
Containers: {},
|
||||
},
|
||||
];
|
||||
|
||||
it('should copy network settings while clearing mac address', () => {
|
||||
const config: ContainerDetailsJSON = {
|
||||
Config: {
|
||||
Hostname: 'test-host',
|
||||
Domainname: 'test-domain',
|
||||
},
|
||||
HostConfig: {
|
||||
NetworkMode: 'bridge',
|
||||
Dns: ['8.8.8.8', '8.8.4.4'],
|
||||
ExtraHosts: ['host1:127.0.0.1'],
|
||||
},
|
||||
NetworkSettings: {
|
||||
Networks: {
|
||||
bridge: {
|
||||
MacAddress: '02:42:ac:11:00:02',
|
||||
IPAMConfig: {
|
||||
IPv4Address: '172.17.0.2',
|
||||
IPv6Address: 'fe80::42:acff:fe11:2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = toViewModel(config, mockNetworks);
|
||||
|
||||
expect(result.macAddress).toBe('');
|
||||
expect(result.hostname).toBe('test-host');
|
||||
expect(result.domain).toBe('test-domain');
|
||||
expect(result.ipv4Address).toBe('172.17.0.2');
|
||||
expect(result.ipv6Address).toBe('fe80::42:acff:fe11:2');
|
||||
});
|
||||
|
||||
it('should return empty MAC address for new containers', () => {
|
||||
const config: ContainerDetailsJSON = {
|
||||
Config: {},
|
||||
HostConfig: { NetworkMode: 'bridge' },
|
||||
};
|
||||
|
||||
const result = toViewModel(config, mockNetworks);
|
||||
|
||||
expect(result.macAddress).toBe('');
|
||||
});
|
||||
|
||||
it('should not duplicate MAC address when duplicating containers', () => {
|
||||
const config: ContainerDetailsJSON = {
|
||||
Config: {
|
||||
Hostname: 'original-container',
|
||||
},
|
||||
HostConfig: {
|
||||
NetworkMode: 'bridge',
|
||||
},
|
||||
NetworkSettings: {
|
||||
Networks: {
|
||||
bridge: {
|
||||
MacAddress: '02:42:ac:11:00:99',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = toViewModel(config, mockNetworks);
|
||||
|
||||
expect(result.macAddress).toBe('');
|
||||
expect(result.hostname).toBe('original-container');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,13 +53,11 @@ export function toViewModel(
|
||||
ipv6Address = networkSettings.IPAMConfig.IPv6Address || '';
|
||||
}
|
||||
|
||||
const macAddress = networkSettings?.MacAddress || '';
|
||||
|
||||
return {
|
||||
networkMode,
|
||||
hostname: config.Config?.Hostname || '',
|
||||
domain: config.Config?.Domainname || '',
|
||||
macAddress,
|
||||
macAddress: '', // mac address is cleared between edit/duplicate
|
||||
ipv4Address,
|
||||
ipv6Address,
|
||||
primaryDns,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormikErrors } from 'formik';
|
||||
import { array, object, SchemaOf, string } from 'yup';
|
||||
import { DeviceMapping } from 'docker-types/generated/1.41';
|
||||
import { DeviceMapping } from 'docker-types/generated/1.44';
|
||||
|
||||
import { FormError } from '@@/form-components/FormError';
|
||||
import { InputList, ItemProps } from '@@/form-components/InputList';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DeviceRequest } from 'docker-types/generated/1.41';
|
||||
import { DeviceRequest } from 'docker-types/generated/1.44';
|
||||
|
||||
import { Values } from './types';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DeviceRequest } from 'docker-types/generated/1.41';
|
||||
import { DeviceRequest } from 'docker-types/generated/1.44';
|
||||
|
||||
import { Values } from './types';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
ContainerConfig,
|
||||
HostConfig,
|
||||
NetworkingConfig,
|
||||
} from 'docker-types/generated/1.41';
|
||||
} from 'docker-types/generated/1.44';
|
||||
|
||||
export interface CreateContainerRequest extends ContainerConfig {
|
||||
HostConfig: HostConfig;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Network } from 'lucide-react';
|
||||
import { EndpointSettings, NetworkSettings } from 'docker-types/generated/1.41';
|
||||
import { EndpointSettings, NetworkSettings } from 'docker-types/generated/1.44';
|
||||
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TableMeta } from '@tanstack/react-table';
|
||||
import { EndpointSettings } from 'docker-types/generated/1.41';
|
||||
import { EndpointSettings } from 'docker-types/generated/1.44';
|
||||
|
||||
export type TableNetwork = EndpointSettings & { id: string; name: string };
|
||||
|
||||
@@ -11,5 +11,5 @@ export type ContainerNetworkTableMeta = TableMeta<TableNetwork> & {
|
||||
export function isContainerNetworkTableMeta(
|
||||
meta?: TableMeta<TableNetwork>
|
||||
): meta is ContainerNetworkTableMeta {
|
||||
return !!meta && meta.table === 'container-networks';
|
||||
return !!meta && 'table' in meta && meta.table === 'container-networks';
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
HostConfig,
|
||||
MountPoint,
|
||||
NetworkSettings,
|
||||
} from 'docker-types/generated/1.41';
|
||||
} from 'docker-types/generated/1.44';
|
||||
|
||||
import { PortainerResponse } from '@/react/docker/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Resources, RestartPolicy } from 'docker-types/generated/1.41';
|
||||
import { Resources, RestartPolicy } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ContainerSummary } from 'docker-types/generated/1.41';
|
||||
import { ContainerSummary } from 'docker-types/generated/1.44';
|
||||
|
||||
import { PortainerResponse } from '@/react/docker/types';
|
||||
import { WithRequiredProperties } from '@/types';
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { toListViewModel } from './utils';
|
||||
import { DockerContainerResponse } from './types/response';
|
||||
|
||||
describe('toListViewModel', () => {
|
||||
function createMockResponse(
|
||||
overrides: Partial<DockerContainerResponse> = {}
|
||||
): DockerContainerResponse {
|
||||
return {
|
||||
Id: 'container123',
|
||||
Names: ['/test-container'],
|
||||
Image: 'nginx:latest',
|
||||
ImageID: 'sha256:abc123',
|
||||
Command: 'nginx -g daemon off;',
|
||||
Created: 1234567890,
|
||||
State: 'running',
|
||||
Status: 'Up 2 hours',
|
||||
Ports: [],
|
||||
Labels: {},
|
||||
SizeRw: 0,
|
||||
SizeRootFs: 0,
|
||||
HostConfig: { NetworkMode: 'bridge' },
|
||||
NetworkSettings: { Networks: {} },
|
||||
Mounts: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('Names field handling', () => {
|
||||
it('should remove leading slash from container names', () => {
|
||||
const response = createMockResponse({
|
||||
Names: ['/container1', '/container2'],
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
expect(result.Names).toEqual(['container1', 'container2']);
|
||||
});
|
||||
|
||||
it('should keep names without leading slash unchanged', () => {
|
||||
const response = createMockResponse({
|
||||
Names: ['container1', 'container2'],
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
expect(result.Names).toEqual(['container1', 'container2']);
|
||||
});
|
||||
|
||||
it('should handle mixed names with and without leading slashes', () => {
|
||||
const response = createMockResponse({
|
||||
Names: ['/container1', 'container2', '/container3'],
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
expect(result.Names).toEqual(['container1', 'container2', 'container3']);
|
||||
});
|
||||
|
||||
it('should handle empty string names', () => {
|
||||
const response = createMockResponse({
|
||||
Names: [''],
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
expect(result.Names).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should handle names that are only a slash', () => {
|
||||
const response = createMockResponse({
|
||||
Names: ['/'],
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
expect(result.Names).toEqual(['']);
|
||||
});
|
||||
|
||||
it('should return default empty name when Names is undefined', () => {
|
||||
const response = createMockResponse({
|
||||
Names: undefined,
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
expect(result.Names).toEqual(['<empty_name>']);
|
||||
});
|
||||
|
||||
it('should return default empty name when Names is empty array', () => {
|
||||
const response = createMockResponse({
|
||||
Names: [],
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
expect(result.Names).toEqual(['<empty_name>']);
|
||||
});
|
||||
|
||||
it('should handle names with multiple leading slashes', () => {
|
||||
const response = createMockResponse({
|
||||
Names: ['//container1', '///container2'],
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
// Note: The function only removes the first character if it's a slash
|
||||
expect(result.Names).toEqual(['/container1', '//container2']);
|
||||
});
|
||||
|
||||
it('should handle names with slashes in the middle', () => {
|
||||
const response = createMockResponse({
|
||||
Names: ['/container/name', 'another/container'],
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
expect(result.Names).toEqual(['container/name', 'another/container']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Full integration tests', () => {
|
||||
it('should transform complete response correctly', () => {
|
||||
const response = createMockResponse({
|
||||
Names: ['/my-container'],
|
||||
Status: 'Up 5 minutes',
|
||||
Labels: {
|
||||
'com.docker.compose.project': 'my-stack',
|
||||
},
|
||||
NetworkSettings: {
|
||||
Networks: {
|
||||
bridge: {
|
||||
IPAddress: '172.17.0.2',
|
||||
Gateway: '172.17.0.1',
|
||||
},
|
||||
},
|
||||
},
|
||||
Ports: [
|
||||
{
|
||||
IP: '0.0.0.0',
|
||||
PrivatePort: 80,
|
||||
PublicPort: 8080,
|
||||
Type: 'tcp',
|
||||
},
|
||||
],
|
||||
Portainer: {
|
||||
ResourceControl: {
|
||||
Id: 1,
|
||||
ResourceId: 'container123',
|
||||
Type: 1,
|
||||
AdministratorsOnly: false,
|
||||
Public: false,
|
||||
System: false,
|
||||
TeamAccesses: [],
|
||||
UserAccesses: [],
|
||||
},
|
||||
Agent: {
|
||||
NodeName: 'node1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = toListViewModel(response);
|
||||
|
||||
expect(result.Names).toEqual(['my-container']);
|
||||
expect(result.IP).toBe('172.17.0.2');
|
||||
expect(result.StackName).toBe('my-stack');
|
||||
expect(result.NodeName).toBe('node1');
|
||||
expect(result.Ports).toHaveLength(1);
|
||||
expect(result.StatusText).toBe('Up 5 minutes');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -43,11 +43,15 @@ export function toListViewModel(
|
||||
)
|
||||
);
|
||||
|
||||
const names = response.Names?.map((n) => {
|
||||
let names = response.Names?.map((n) => {
|
||||
const nameWithoutSlash = n[0] === '/' ? n.slice(1) : n;
|
||||
return nameWithoutSlash;
|
||||
});
|
||||
|
||||
if (!names || names.length === 0) {
|
||||
names = ['<empty_name>'];
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
ResourceControl: resourceControl,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
import { Clock } from 'lucide-react';
|
||||
import { EventMessage } from 'docker-types/generated/1.41';
|
||||
import { EventMessage } from 'docker-types/generated/1.44';
|
||||
|
||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EventMessage } from 'docker-types/generated/1.41';
|
||||
import { EventMessage } from 'docker-types/generated/1.44';
|
||||
|
||||
type EventType = NonNullable<EventMessage['Type']>;
|
||||
type Action = string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IPAMConfig } from 'docker-types/generated/1.41';
|
||||
import { IPAMConfig } from 'docker-types/generated/1.44';
|
||||
|
||||
import { NetworkViewModel } from '@/docker/models/network';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EndpointSettings } from 'docker-types/generated/1.41';
|
||||
import { EndpointSettings } from 'docker-types/generated/1.44';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Network } from 'docker-types/generated/1.41';
|
||||
import { Network } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ImageInspect } from 'docker-types/generated/1.41';
|
||||
import { ImageInspect } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ImageSummary } from 'docker-types/generated/1.41';
|
||||
import { ImageSummary } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Node } from 'docker-types/generated/1.41';
|
||||
import { Node } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Node } from 'docker-types/generated/1.41';
|
||||
import { Node } from 'docker-types/generated/1.44';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Node, NodeSpec } from 'docker-types/generated/1.41';
|
||||
import { Node, NodeSpec } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SecretSpec } from 'docker-types/generated/1.41';
|
||||
import { SecretSpec } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Secret } from 'docker-types/generated/1.41';
|
||||
import { Secret } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Secret } from 'docker-types/generated/1.41';
|
||||
import { Secret } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Task } from 'docker-types/generated/1.41';
|
||||
import { Task } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EventMessage } from 'docker-types/generated/1.41';
|
||||
import { EventMessage } from 'docker-types/generated/1.44';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import axios, {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { SystemInfo } from 'docker-types/generated/1.41';
|
||||
import { SystemInfo } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
Plugin,
|
||||
PluginInterfaceType,
|
||||
PluginsInfo,
|
||||
} from 'docker-types/generated/1.41';
|
||||
} from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
@@ -87,7 +87,7 @@ export function aggregateData(
|
||||
(plugin) =>
|
||||
plugin.Enabled &&
|
||||
// docker has an error in their types, so we need to cast to unknown first
|
||||
// see https://docs.docker.com/engine/api/v1.41/#tag/Plugin/operation/PluginList
|
||||
// see https://docs.docker.com/engine/api/v1.44/#tag/Plugin/operation/PluginList
|
||||
plugin.Config.Interface.Types.includes(
|
||||
pluginTypeToVersionMap[pluginType] as unknown as PluginInterfaceType
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Swarm } from 'docker-types/generated/1.41';
|
||||
import { Swarm } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { SystemVersion } from 'docker-types/generated/1.41';
|
||||
import { SystemVersion } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EndpointPortConfig } from 'docker-types/generated/1.41';
|
||||
import { EndpointPortConfig } from 'docker-types/generated/1.44';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { Values } from './PortsMappingField';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EndpointPortConfig } from 'docker-types/generated/1.41';
|
||||
import { EndpointPortConfig } from 'docker-types/generated/1.44';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { PortBinding, Protocol, Value, isProtocol, isRange } from './types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Node } from 'docker-types/generated/1.41';
|
||||
import { Node } from 'docker-types/generated/1.44';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
|
||||
import { useNodes } from '@/react/docker/proxy/queries/nodes/useNodes';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Node } from 'docker-types/generated/1.41';
|
||||
import { Node } from 'docker-types/generated/1.44';
|
||||
|
||||
import { ServiceViewModel } from '@/docker/models/service';
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
import { Node } from 'docker-types/generated/1.41';
|
||||
import { Node } from 'docker-types/generated/1.44';
|
||||
|
||||
import { ServiceViewModel } from '@/docker/models/service';
|
||||
import { useNodes } from '@/react/docker/proxy/queries/nodes/useNodes';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Service } from 'docker-types/generated/1.41';
|
||||
import { Service } from 'docker-types/generated/1.44';
|
||||
|
||||
import { ServiceUpdateConfig } from '../types';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Service } from 'docker-types/generated/1.41';
|
||||
import { Service } from 'docker-types/generated/1.44';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Service } from 'docker-types/generated/1.41';
|
||||
import { Service } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Service } from 'docker-types/generated/1.41';
|
||||
import { Service } from 'docker-types/generated/1.44';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ServiceUpdateResponse } from 'docker-types/generated/1.41';
|
||||
import { ServiceUpdateResponse } from 'docker-types/generated/1.44';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user