Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aae5e533c6 | |||
| d54ccd5502 | |||
| 7e526c4df7 | |||
| bf56a6c913 | |||
| 6b1b6ff998 | |||
| 9183be7a8c | |||
| 7f83d15812 | |||
| f926b61978 | |||
| cc5f790f98 | |||
| 40b210a708 | |||
| 8be327f087 | |||
| f498d76c4f | |||
| a1fa77cbe4 | |||
| e11c2e9611 | |||
| 6e03a801e6 | |||
| 6024a97892 | |||
| 1549b36103 | |||
| 7bb3e0f7a6 | |||
| 49cc901dc3 | |||
| 31dd62fbcc | |||
| a7d2d134d0 | |||
| 14f25f1e88 | |||
| 69b8b8373f | |||
| 0b075e6e10 |
@@ -114,6 +114,9 @@ overrides:
|
||||
'@typescript-eslint/explicit-module-boundary-types': off
|
||||
'@typescript-eslint/no-unused-vars': 'error'
|
||||
'@typescript-eslint/no-explicit-any': 'error'
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'error'
|
||||
'consistent-return': 'off'
|
||||
'default-case': off
|
||||
'jsx-a11y/label-has-associated-control':
|
||||
- error
|
||||
- assert: either
|
||||
|
||||
+9
-7
@@ -1,6 +1,7 @@
|
||||
import path from 'path';
|
||||
|
||||
import { StorybookConfig } from '@storybook/react-webpack5';
|
||||
|
||||
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
|
||||
import { Configuration } from 'webpack';
|
||||
import postcss from 'postcss';
|
||||
|
||||
@@ -85,12 +86,7 @@ const config: StorybookConfig = {
|
||||
...config,
|
||||
resolve: {
|
||||
...config.resolve,
|
||||
plugins: [
|
||||
...(config.resolve?.plugins || []),
|
||||
new TsconfigPathsPlugin({
|
||||
extensions: config.resolve?.extensions,
|
||||
}),
|
||||
],
|
||||
tsconfig: path.resolve(__dirname, '..', 'tsconfig.json'),
|
||||
},
|
||||
module: {
|
||||
...config.module,
|
||||
@@ -101,11 +97,17 @@ const config: StorybookConfig = {
|
||||
staticDirs: ['./public'],
|
||||
typescript: {
|
||||
reactDocgen: 'react-docgen-typescript',
|
||||
reactDocgenTypescriptOptions: {
|
||||
compilerOptions: {
|
||||
outDir: path.resolve(__dirname, '..', 'dist/public'),
|
||||
},
|
||||
},
|
||||
},
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {},
|
||||
},
|
||||
docs: {},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -27,9 +27,13 @@ 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
|
||||
# Frontend
|
||||
pnpm dev # Webpack dev server
|
||||
pnpm build # Build frontend with webpack
|
||||
pnpm typecheck # Run typecheck for frontend (with tsc)
|
||||
pnpm lint # lint frontend (with eslint)
|
||||
pnpm test # test frontend (with vitest)
|
||||
pnpm format # format frontend (with prettier)
|
||||
|
||||
# Testing
|
||||
make test # All tests (backend + frontend)
|
||||
|
||||
@@ -4,7 +4,8 @@ WEBPACK_CONFIG=webpack/webpack.$(ENV).js
|
||||
TAG=local
|
||||
|
||||
SWAG=go run github.com/swaggo/swag/cmd/swag@v1.16.2
|
||||
GOTESTSUM=go run gotest.tools/gotestsum@latest
|
||||
GOTESTSUM_VERSION?=v1.13.0
|
||||
GOTESTSUM=go run gotest.tools/gotestsum@$(GOTESTSUM_VERSION)
|
||||
|
||||
# Don't change anything below this line unless you know what you're doing
|
||||
.DEFAULT_GOAL := help
|
||||
@@ -57,8 +58,10 @@ test: test-server test-client ## Run all tests
|
||||
test-client: ## Run client tests
|
||||
pnpm run test $(ARGS) --coverage
|
||||
|
||||
TEST_PACKAGES?=./...
|
||||
|
||||
test-server: ## Run server tests
|
||||
$(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover -covermode=atomic -coverprofile=coverage.out ./...
|
||||
$(GOTESTSUM) --format pkgname-and-test-fails --format-hide-empty-pkg --hide-summary skipped -- -cover -covermode=atomic -coverprofile=coverage.out $(TEST_PACKAGES)
|
||||
|
||||
##@ Dev
|
||||
.PHONY: dev dev-client dev-server
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) GetEncodedAuthorizationToken() (token *string, expiry *time.Time, err error) {
|
||||
getAuthorizationTokenOutput, err := s.client.GetAuthorizationToken(context.TODO(), nil)
|
||||
func (s *Service) GetEncodedAuthorizationToken(ctx context.Context) (token *string, expiry *time.Time, err error) {
|
||||
getAuthorizationTokenOutput, err := s.client.GetAuthorizationToken(ctx, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -27,8 +27,8 @@ func (s *Service) GetEncodedAuthorizationToken() (token *string, expiry *time.Ti
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Service) GetAuthorizationToken() (token *string, expiry *time.Time, err error) {
|
||||
tokenEncodedStr, expiry, err := s.GetEncodedAuthorizationToken()
|
||||
func (s *Service) GetAuthorizationToken(ctx context.Context) (token *string, expiry *time.Time, err error) {
|
||||
tokenEncodedStr, expiry, err := s.GetEncodedAuthorizationToken(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
+9
-2
@@ -94,13 +94,20 @@ 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(
|
||||
var hasKubectlShellImageFlag bool
|
||||
kubectlShellImageFlag := kingpin.Flag(
|
||||
"kubectl-shell-image",
|
||||
"Kubectl shell image",
|
||||
).Envar(portainer.KubectlShellImageEnvVar).Default(portainer.DefaultKubectlShellImage).String()
|
||||
).Envar(portainer.KubectlShellImageEnvVar).
|
||||
Default(portainer.DefaultKubectlShellImage).
|
||||
IsSetByUser(&hasKubectlShellImageFlag)
|
||||
flags.KubectlShellImage = kubectlShellImageFlag.String()
|
||||
|
||||
kingpin.Parse()
|
||||
|
||||
_, kubectlShellImageEnvVarSet := os.LookupEnv(portainer.KubectlShellImageEnvVar)
|
||||
flags.KubectlShellImageSet = hasKubectlShellImageFlag || kubectlShellImageEnvVarSet
|
||||
|
||||
if !filepath.IsAbs(*flags.Assets) {
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
zerolog "github.com/rs/zerolog/log"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -26,6 +27,59 @@ func TestOptionParser(t *testing.T) {
|
||||
require.True(t, *opts.EnableEdgeComputeFeatures)
|
||||
}
|
||||
|
||||
func TestParseKubectlShellImageFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
envVars map[string]string
|
||||
expectedKubectlShellImageSet bool
|
||||
expectedKubectlShellFlag string
|
||||
}{
|
||||
{
|
||||
name: "no flag, no env var",
|
||||
expectedKubectlShellImageSet: false,
|
||||
expectedKubectlShellFlag: portainer.DefaultKubectlShellImage,
|
||||
},
|
||||
{
|
||||
name: "explicit flag",
|
||||
args: []string{"portainer", "--kubectl-shell-image=myimage:v2"},
|
||||
expectedKubectlShellImageSet: true,
|
||||
expectedKubectlShellFlag: "myimage:v2",
|
||||
},
|
||||
{
|
||||
name: "env var",
|
||||
envVars: map[string]string{portainer.KubectlShellImageEnvVar: "myimage:v3"},
|
||||
expectedKubectlShellImageSet: true,
|
||||
expectedKubectlShellFlag: "myimage:v3",
|
||||
},
|
||||
{
|
||||
name: "both env var and flag set",
|
||||
args: []string{"portainer", "--kubectl-shell-image=myimage:v2"},
|
||||
envVars: map[string]string{portainer.KubectlShellImageEnvVar: "myimage:v3"},
|
||||
expectedKubectlShellImageSet: true,
|
||||
expectedKubectlShellFlag: "myimage:v2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.args == nil {
|
||||
tc.args = []string{"portainer"}
|
||||
}
|
||||
setOsArgs(t, tc.args)
|
||||
|
||||
for k, v := range tc.envVars {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
|
||||
flags, err := Service{}.ParseFlags("test-version")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedKubectlShellImageSet, flags.KubectlShellImageSet)
|
||||
require.Equal(t, tc.expectedKubectlShellFlag, *flags.KubectlShellImage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTLSFlags(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -248,6 +248,10 @@ func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.
|
||||
settings.EnableEdgeComputeFeatures = cmp.Or(*flags.EnableEdgeComputeFeatures, settings.EnableEdgeComputeFeatures)
|
||||
settings.TemplatesURL = cmp.Or(*flags.Templates, settings.TemplatesURL)
|
||||
|
||||
if flags.KubectlShellImageSet {
|
||||
settings.KubectlShellImage = *flags.KubectlShellImage
|
||||
}
|
||||
|
||||
if *flags.Labels != nil {
|
||||
settings.BlackListedLabels = *flags.Labels
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -12,7 +15,7 @@ import (
|
||||
const secretFileName = "secret.txt"
|
||||
|
||||
func createPasswordFile(t *testing.T, secretPath, password string) string {
|
||||
err := os.WriteFile(secretPath, []byte(password), 0600)
|
||||
err := os.WriteFile(secretPath, []byte(password), 0o600)
|
||||
require.NoError(t, err)
|
||||
return secretPath
|
||||
}
|
||||
@@ -38,6 +41,65 @@ func TestLoadEncryptionSecretKey(t *testing.T) {
|
||||
require.Len(t, encryptionKey, 32)
|
||||
}
|
||||
|
||||
func TestUpdateSettingsFromFlags_KubectlShellImage(t *testing.T) {
|
||||
const existingImage = "existing-image:v1"
|
||||
const newImage = "new-image:v2"
|
||||
|
||||
emptyString := ""
|
||||
falseBool := false
|
||||
var emptyLabels []portainer.Pair
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
imageSet bool
|
||||
flagImage string
|
||||
expectedKubectlShellImage string
|
||||
}{
|
||||
{
|
||||
name: "flag not set — DB image unchanged",
|
||||
imageSet: false,
|
||||
flagImage: portainer.DefaultKubectlShellImage,
|
||||
expectedKubectlShellImage: existingImage,
|
||||
},
|
||||
{
|
||||
name: "flag set — DB image updated",
|
||||
imageSet: true,
|
||||
flagImage: newImage,
|
||||
expectedKubectlShellImage: newImage,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
store := testhelpers.NewDatastore(
|
||||
testhelpers.WithSettingsService(&portainer.Settings{
|
||||
KubectlShellImage: existingImage,
|
||||
}),
|
||||
testhelpers.WithSSLSettingsService(&portainer.SSLSettings{}),
|
||||
)
|
||||
|
||||
flags := &portainer.CLIFlags{
|
||||
SnapshotInterval: &emptyString,
|
||||
Logo: &emptyString,
|
||||
EnableEdgeComputeFeatures: &falseBool,
|
||||
Templates: &emptyString,
|
||||
Labels: &emptyLabels,
|
||||
HTTPDisabled: &falseBool,
|
||||
HTTPEnabled: &falseBool,
|
||||
}
|
||||
flags.KubectlShellImage = &tc.flagImage
|
||||
flags.KubectlShellImageSet = tc.imageSet
|
||||
|
||||
err := updateSettingsFromFlags(store, flags)
|
||||
require.NoError(t, err)
|
||||
|
||||
settings, err := store.Settings().Settings()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedKubectlShellImage, settings.KubectlShellImage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBSecretPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
keyFilenameFlag string
|
||||
|
||||
@@ -59,6 +59,7 @@ func (store *Store) checkOrCreateDefaultSettings() error {
|
||||
KubectlShellImage: *store.flags.KubectlShellImage,
|
||||
|
||||
IsDockerDesktopExtension: isDDExtention,
|
||||
EnforceEdgeID: true,
|
||||
}
|
||||
|
||||
return store.SettingsService.UpdateSettings(defaultSettings)
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
"RequiredPasswordLength": 12
|
||||
},
|
||||
"KubeconfigExpiry": "0",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.39.1",
|
||||
"KubectlShellImage": "portainer/kubectl-shell:2.39.2",
|
||||
"LDAPSettings": {
|
||||
"AnonymousMode": true,
|
||||
"AutoCreateUsers": true,
|
||||
@@ -942,7 +942,7 @@
|
||||
}
|
||||
],
|
||||
"version": {
|
||||
"VERSION": "{\"SchemaVersion\":\"2.39.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
"VERSION": "{\"SchemaVersion\":\"2.39.2\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
|
||||
},
|
||||
"webhooks": null
|
||||
}
|
||||
+11
-15
@@ -66,7 +66,7 @@ func (manager *ComposeStackManager) Up(ctx context.Context, stack *portainer.Sta
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||
Registries: portainerRegistriesToAuthConfigs(options.Registries),
|
||||
},
|
||||
ForceRecreate: options.ForceRecreate,
|
||||
AbortOnContainerExit: options.AbortOnContainerExit,
|
||||
@@ -97,7 +97,7 @@ func (manager *ComposeStackManager) Run(ctx context.Context, stack *portainer.St
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||
Registries: portainerRegistriesToAuthConfigs(options.Registries),
|
||||
},
|
||||
Remove: options.Remove,
|
||||
Args: options.Args,
|
||||
@@ -146,7 +146,7 @@ func (manager *ComposeStackManager) Pull(ctx context.Context, stack *portainer.S
|
||||
EnvFilePath: envFilePath,
|
||||
Host: url,
|
||||
ProjectName: stack.Name,
|
||||
Registries: portainerRegistriesToAuthConfigs(manager.dataStore, options.Registries),
|
||||
Registries: portainerRegistriesToAuthConfigs(options.Registries),
|
||||
})
|
||||
return errors.Wrap(err, "failed to pull images of the stack")
|
||||
}
|
||||
@@ -229,7 +229,12 @@ func copyConfigEnvVars(w io.Writer, envs []portainer.Pair) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func portainerRegistriesToAuthConfigs(tx dataservices.DataStoreTx, registries []portainer.Registry) []types.AuthConfig {
|
||||
// portainerRegistriesToAuthConfigs converts registries to Docker auth configs.
|
||||
// Callers must ensure ECR tokens are valid before calling this function (e.g. via
|
||||
// registryutils.ValidateRegistriesECRTokens with a real DataStoreTx). This function
|
||||
// intentionally performs no DB writes to avoid write-lock contention when called inside
|
||||
// an active BoltDB write transaction.
|
||||
func portainerRegistriesToAuthConfigs(registries []portainer.Registry) []types.AuthConfig {
|
||||
var authConfigs []types.AuthConfig
|
||||
|
||||
for _, r := range registries {
|
||||
@@ -242,7 +247,7 @@ func portainerRegistriesToAuthConfigs(tx dataservices.DataStoreTx, registries []
|
||||
if r.Authentication {
|
||||
var err error
|
||||
|
||||
ac.Username, ac.Password, err = getEffectiveRegUsernamePassword(tx, &r)
|
||||
ac.Username, ac.Password, err = getEffectiveRegUsernamePassword(&r)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -254,16 +259,7 @@ func portainerRegistriesToAuthConfigs(tx dataservices.DataStoreTx, registries []
|
||||
return authConfigs
|
||||
}
|
||||
|
||||
func getEffectiveRegUsernamePassword(tx dataservices.DataStoreTx, registry *portainer.Registry) (string, string, error) {
|
||||
if err := registryutils.EnsureRegTokenValid(tx, registry); err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("RegistryName", registry.Name).
|
||||
Msg("Failed to validate registry token. Skip logging with this registry.")
|
||||
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
func getEffectiveRegUsernamePassword(registry *portainer.Registry) (string, string, error) {
|
||||
username, password, err := registryutils.GetRegEffectiveCredential(registry)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
@@ -94,3 +95,74 @@ func Test_createEnvFile_mergesDefultAndInplaceEnvVars(t *testing.T) {
|
||||
|
||||
assert.Equal(t, []byte("VAR1=VAL1\nVAR2=VAL2\n\nVAR1=NEW_VAL1\nVAR3=VAL3\n"), content)
|
||||
}
|
||||
|
||||
func Test_portainerRegistriesToAuthConfigs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("returns empty slice for empty input", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := portainerRegistriesToAuthConfigs([]portainer.Registry{})
|
||||
require.Nil(t, result)
|
||||
})
|
||||
|
||||
t.Run("uses registry URL, username and password for non-authenticated registry", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
registries := []portainer.Registry{
|
||||
{URL: "registry.example.com", Username: "user", Password: "pass", Authentication: false},
|
||||
}
|
||||
result := portainerRegistriesToAuthConfigs(registries)
|
||||
require.Len(t, result, 1)
|
||||
require.Equal(t, "registry.example.com", result[0].ServerAddress)
|
||||
require.Equal(t, "user", result[0].Username)
|
||||
require.Equal(t, "pass", result[0].Password)
|
||||
})
|
||||
|
||||
t.Run("uses username and password for authenticated non-ECR registry", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
registries := []portainer.Registry{
|
||||
{URL: "registry.example.com", Username: "user", Password: "pass", Authentication: true, Type: portainer.CustomRegistry},
|
||||
}
|
||||
result := portainerRegistriesToAuthConfigs(registries)
|
||||
require.Len(t, result, 1)
|
||||
require.Equal(t, "user", result[0].Username)
|
||||
require.Equal(t, "pass", result[0].Password)
|
||||
})
|
||||
|
||||
t.Run("parses ECR access token for authenticated ECR registry with valid token", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
registries := []portainer.Registry{
|
||||
{
|
||||
URL: "123456789.dkr.ecr.us-east-1.amazonaws.com",
|
||||
Username: "AKIAIOSFODNN7EXAMPLE",
|
||||
Password: "secretkey",
|
||||
Authentication: true,
|
||||
Type: portainer.EcrRegistry,
|
||||
Ecr: portainer.EcrData{Region: "us-east-1"},
|
||||
AccessToken: "AWS:ecr-password",
|
||||
AccessTokenExpiry: time.Now().Add(time.Hour).Unix(),
|
||||
},
|
||||
}
|
||||
result := portainerRegistriesToAuthConfigs(registries)
|
||||
require.Len(t, result, 1)
|
||||
require.Equal(t, "AWS", result[0].Username)
|
||||
require.Equal(t, "ecr-password", result[0].Password)
|
||||
})
|
||||
|
||||
t.Run("includes valid registries and skips ones with credential errors", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
registries := []portainer.Registry{
|
||||
{URL: "valid.example.com", Username: "user", Password: "pass", Authentication: false},
|
||||
{
|
||||
URL: "123456789.dkr.ecr.us-east-1.amazonaws.com",
|
||||
Authentication: true,
|
||||
Type: portainer.EcrRegistry,
|
||||
Ecr: portainer.EcrData{Region: "us-east-1"},
|
||||
AccessToken: "no-colon-token",
|
||||
AccessTokenExpiry: time.Now().Add(time.Hour).Unix(),
|
||||
},
|
||||
}
|
||||
result := portainerRegistriesToAuthConfigs(registries)
|
||||
require.Len(t, result, 1)
|
||||
require.Equal(t, "valid.example.com", result[0].ServerAddress)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func (manager *SwarmStackManager) Login(registries []portainer.Registry, endpoin
|
||||
|
||||
for _, registry := range registries {
|
||||
if registry.Authentication {
|
||||
username, password, err := getEffectiveRegUsernamePassword(manager.dataStore, ®istry)
|
||||
username, password, err := getEffectiveRegUsernamePassword(®istry)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
+51
-20
@@ -3,23 +3,42 @@ package git
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/go-git/go-billy/v5"
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/cache"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
gogitfs "github.com/go-git/go-git/v5/storage/filesystem"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// noSymlinkFS wraps a billy.Filesystem and rejects symlink creation to prevent
|
||||
// symlink traversal attacks from untrusted git repositories
|
||||
type noSymlinkFS struct {
|
||||
billy.Filesystem
|
||||
}
|
||||
|
||||
func (fs noSymlinkFS) Symlink(_, _ string) error {
|
||||
return gittypes.ErrSymlinkDetected
|
||||
}
|
||||
|
||||
// NewNoSymlinkFS wraps fs and rejects any symlink creation
|
||||
func NewNoSymlinkFS(fs billy.Filesystem) billy.Filesystem {
|
||||
return noSymlinkFS{fs}
|
||||
}
|
||||
|
||||
type gitClient struct {
|
||||
preserveGitDirectory bool
|
||||
}
|
||||
@@ -30,8 +49,33 @@ func NewGitClient(preserveGitDir bool) *gitClient {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *gitClient) Download(ctx context.Context, dst string, opt *git.CloneOptions) error {
|
||||
wt := NewNoSymlinkFS(osfs.New(dst))
|
||||
dot := osfs.New(filesystem.JoinPaths(dst, ".git"))
|
||||
storer := gogitfs.NewStorage(dot, cache.NewObjectLRU(0))
|
||||
|
||||
_, err := git.CloneContext(ctx, storer, wt, opt)
|
||||
if err != nil {
|
||||
if err.Error() == "authentication required" {
|
||||
return gittypes.ErrAuthenticationFailure
|
||||
}
|
||||
|
||||
return errors.Wrap(err, "failed to clone git repository")
|
||||
}
|
||||
|
||||
if c.preserveGitDirectory {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(filesystem.JoinPaths(dst, ".git")); err != nil {
|
||||
log.Error().Err(err).Msg("failed to remove .git directory")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) error {
|
||||
gitOptions := git.CloneOptions{
|
||||
gitOptions := &git.CloneOptions{
|
||||
URL: opt.repositoryUrl,
|
||||
Depth: opt.depth,
|
||||
InsecureSkipTLS: opt.tlsSkipVerify,
|
||||
@@ -43,23 +87,7 @@ func (c *gitClient) download(ctx context.Context, dst string, opt cloneOption) e
|
||||
gitOptions.ReferenceName = plumbing.ReferenceName(opt.referenceName)
|
||||
}
|
||||
|
||||
_, err := git.PlainCloneContext(ctx, dst, false, &gitOptions)
|
||||
|
||||
if err != nil {
|
||||
if err.Error() == "authentication required" {
|
||||
return gittypes.ErrAuthenticationFailure
|
||||
}
|
||||
return errors.Wrap(err, "failed to clone git repository")
|
||||
}
|
||||
|
||||
if !c.preserveGitDirectory {
|
||||
err := os.RemoveAll(filepath.Join(dst, ".git"))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to remove .git directory")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return c.Download(ctx, dst, gitOptions)
|
||||
}
|
||||
|
||||
func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string, error) {
|
||||
@@ -78,6 +106,7 @@ func (c *gitClient) latestCommitID(ctx context.Context, opt fetchOption) (string
|
||||
if err.Error() == "authentication required" {
|
||||
return "", gittypes.ErrAuthenticationFailure
|
||||
}
|
||||
|
||||
return "", errors.Wrap(err, "failed to list repository refs")
|
||||
}
|
||||
|
||||
@@ -159,6 +188,7 @@ func (c *gitClient) listRefs(ctx context.Context, opt baseOption) ([]string, err
|
||||
if ref.Name().String() == "HEAD" {
|
||||
continue
|
||||
}
|
||||
|
||||
ret = append(ret, ref.Name().String())
|
||||
}
|
||||
|
||||
@@ -225,5 +255,6 @@ func checkGitError(err error) error {
|
||||
} else if errMsg == "authentication required" {
|
||||
return gittypes.ErrAuthenticationFailure
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+113
-3
@@ -3,13 +3,17 @@ package git
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
gittypes "github.com/portainer/portainer/api/git/types"
|
||||
|
||||
"github.com/go-git/go-billy/v5/osfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -18,7 +22,7 @@ import (
|
||||
|
||||
func setup(t *testing.T) string {
|
||||
dir := t.TempDir()
|
||||
bareRepoDir := filepath.Join(dir, "test-clone.git")
|
||||
bareRepoDir := filesystem.JoinPaths(dir, "test-clone.git")
|
||||
|
||||
file, err := os.OpenFile("./testdata/test-clone-git-repo.tar.gz", os.O_RDONLY, 0755)
|
||||
if err != nil {
|
||||
@@ -53,7 +57,7 @@ func Test_ClonePublicRepository_NoGitDirectory(t *testing.T) {
|
||||
t.Logf("Cloning into %s", dir)
|
||||
err := service.CloneRepository(dir, repositoryURL, referenceName, "", "", gittypes.GitCredentialAuthType_Basic, false)
|
||||
require.NoError(t, err)
|
||||
assert.NoDirExists(t, filepath.Join(dir, ".git"))
|
||||
assert.NoDirExists(t, filesystem.JoinPaths(dir, ".git"))
|
||||
}
|
||||
|
||||
func Test_cloneRepository(t *testing.T) {
|
||||
@@ -146,6 +150,112 @@ func getCommitHistoryLength(t *testing.T, dir string) int {
|
||||
return count
|
||||
}
|
||||
|
||||
func Test_noSymlinkFS_Symlink(t *testing.T) {
|
||||
fs := NewNoSymlinkFS(osfs.New(t.TempDir()))
|
||||
err := fs.Symlink("../../../etc/passwd", "evil-link")
|
||||
require.ErrorIs(t, err, gittypes.ErrSymlinkDetected)
|
||||
}
|
||||
|
||||
func Test_noSymlinkFS_OtherOperations(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
fs := NewNoSymlinkFS(osfs.New(dir))
|
||||
|
||||
f, err := fs.Create("test.txt")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = f.Write([]byte("hello"))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
info, err := fs.Stat("test.txt")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test.txt", info.Name())
|
||||
}
|
||||
|
||||
func createBareRepoWithSymlink(t *testing.T) string {
|
||||
t.Helper()
|
||||
|
||||
bareDir := filesystem.JoinPaths(t.TempDir(), "symlink-repo.git")
|
||||
|
||||
repo, err := git.PlainInit(bareDir, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
storer := repo.Storer
|
||||
|
||||
fileBlob := &plumbing.MemoryObject{}
|
||||
fileBlob.SetType(plumbing.BlobObject)
|
||||
|
||||
_, err = fileBlob.Write([]byte("hello world\n"))
|
||||
require.NoError(t, err)
|
||||
|
||||
fileHash, err := storer.SetEncodedObject(fileBlob)
|
||||
require.NoError(t, err)
|
||||
|
||||
symlinkBlob := &plumbing.MemoryObject{}
|
||||
symlinkBlob.SetType(plumbing.BlobObject)
|
||||
|
||||
_, err = symlinkBlob.Write([]byte("../../../etc/passwd"))
|
||||
require.NoError(t, err)
|
||||
|
||||
symlinkHash, err := storer.SetEncodedObject(symlinkBlob)
|
||||
require.NoError(t, err)
|
||||
|
||||
tree := &object.Tree{
|
||||
Entries: []object.TreeEntry{
|
||||
{Name: "evil-link", Mode: filemode.Symlink, Hash: symlinkHash},
|
||||
{Name: "file.txt", Mode: filemode.Regular, Hash: fileHash},
|
||||
},
|
||||
}
|
||||
|
||||
treeObj := &plumbing.MemoryObject{}
|
||||
|
||||
err = tree.Encode(treeObj)
|
||||
require.NoError(t, err)
|
||||
|
||||
treeHash, err := storer.SetEncodedObject(treeObj)
|
||||
require.NoError(t, err)
|
||||
|
||||
sig := object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()}
|
||||
commit := &object.Commit{
|
||||
Message: "add symlink",
|
||||
Author: sig,
|
||||
Committer: sig,
|
||||
TreeHash: treeHash,
|
||||
}
|
||||
|
||||
commitObj := &plumbing.MemoryObject{}
|
||||
|
||||
err = commit.Encode(commitObj)
|
||||
require.NoError(t, err)
|
||||
|
||||
commitHash, err := storer.SetEncodedObject(commitObj)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = storer.SetReference(plumbing.NewHashReference("refs/heads/main", commitHash))
|
||||
require.NoError(t, err)
|
||||
|
||||
err = storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, "refs/heads/main"))
|
||||
require.NoError(t, err)
|
||||
|
||||
return bareDir
|
||||
}
|
||||
|
||||
func Test_Download_RejectsSymlink(t *testing.T) {
|
||||
client := NewGitClient(false)
|
||||
repoURL := createBareRepoWithSymlink(t)
|
||||
|
||||
err := client.Download(t.Context(), t.TempDir(), &git.CloneOptions{
|
||||
URL: repoURL,
|
||||
Depth: 1,
|
||||
SingleBranch: true,
|
||||
Tags: git.NoTags,
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, gittypes.ErrSymlinkDetected)
|
||||
}
|
||||
|
||||
func Test_listRefsPrivateRepository(t *testing.T) {
|
||||
ensureIntegrationTest(t)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
var (
|
||||
ErrIncorrectRepositoryURL = errors.New("git repository could not be found, please ensure that the URL is correct")
|
||||
ErrAuthenticationFailure = errors.New("authentication failed, please ensure that the git credentials are correct")
|
||||
ErrSymlinkDetected = errors.New("repository contains a symlink, which is not allowed for security reasons")
|
||||
)
|
||||
|
||||
type GitCredentialAuthType int
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package containers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -46,7 +47,7 @@ func (handler *Handler) recreate(w http.ResponseWriter, r *http.Request) *httper
|
||||
|
||||
agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
|
||||
newContainer, err := handler.containerService.Recreate(r.Context(), endpoint, containerID, payload.PullImage, "", agentTargetHeader)
|
||||
newContainer, err := handler.containerService.Recreate(context.TODO(), endpoint, containerID, payload.PullImage, "", agentTargetHeader)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Error recreating container", err)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func NewHandler(bouncer security.BouncerService) *Handler {
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/agent_versions",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.agentVersions))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/relations", bouncer.RestrictedAccess(httperror.LoggerHandler(h.updateRelations))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/relations", bouncer.AdminAccess(httperror.LoggerHandler(h.updateRelations))).Methods(http.MethodPut)
|
||||
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet)
|
||||
|
||||
@@ -81,7 +81,7 @@ type Handler struct {
|
||||
}
|
||||
|
||||
// @title PortainerCE API
|
||||
// @version 2.39.1
|
||||
// @version 2.39.2
|
||||
// @description.markdown api-description.md
|
||||
// @termsOfService
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ func NewHandler(bouncer security.BouncerService, authorizationService *authoriza
|
||||
namespaceRouter := endpointRouter.PathPrefix("/namespaces/{namespace}").Subrouter()
|
||||
namespaceRouter.Handle("/configmaps/{configmap}", httperror.LoggerHandler(h.getKubernetesConfigMap)).Methods(http.MethodGet)
|
||||
namespaceRouter.Handle("/events", httperror.LoggerHandler(h.getKubernetesEventsForNamespace)).Methods(http.MethodGet)
|
||||
namespaceRouter.Handle("/system", bouncer.RestrictedAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
|
||||
namespaceRouter.Handle("/system", bouncer.AdminAccess(httperror.LoggerHandler(h.namespacesToggleSystem))).Methods(http.MethodPut)
|
||||
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.getKubernetesIngressControllersByNamespace)).Methods(http.MethodGet)
|
||||
namespaceRouter.Handle("/ingresscontrollers", httperror.LoggerHandler(h.updateKubernetesIngressControllersByNamespace)).Methods(http.MethodPut)
|
||||
namespaceRouter.Handle("/ingresses/{ingress}", httperror.LoggerHandler(h.getKubernetesIngress)).Methods(http.MethodGet)
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
// @produce json
|
||||
// @param endpointId query int true "environment(endpoint) ID of the environment(endpoint) where the resource is located"
|
||||
// @param nodeName query string false "node name"
|
||||
// @param token query string true "JWT token used for authentication against this environment(endpoint)"
|
||||
// @success 200
|
||||
// @failure 400
|
||||
// @failure 403
|
||||
|
||||
@@ -32,7 +32,6 @@ type execStartOperationPayload struct {
|
||||
// @produce json
|
||||
// @param endpointId query int true "environment(endpoint) ID of the environment(endpoint) where the resource is located"
|
||||
// @param nodeName query string false "node name"
|
||||
// @param token query string true "JWT token used for authentication against this environment(endpoint)"
|
||||
// @success 200
|
||||
// @failure 400
|
||||
// @failure 409
|
||||
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
// @param podName query string true "name of the pod containing the container"
|
||||
// @param containerName query string true "name of the container"
|
||||
// @param command query string true "command to execute in the container"
|
||||
// @param token query string true "JWT token used for authentication against this environment(endpoint)"
|
||||
// @success 200
|
||||
// @failure 400
|
||||
// @failure 403
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param endpointId query int true "environment(endpoint) ID of the environment(endpoint) where the resource is located"
|
||||
// @param token query string true "JWT token used for authentication against this environment(endpoint)"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
|
||||
@@ -170,18 +170,23 @@ func containerHasBlackListedLabel(containerLabels map[string]any, labelBlackList
|
||||
func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
type PartialContainer struct {
|
||||
HostConfig struct {
|
||||
Privileged bool `json:"Privileged"`
|
||||
PidMode string `json:"PidMode"`
|
||||
Devices []any `json:"Devices"`
|
||||
Sysctls map[string]any `json:"Sysctls"`
|
||||
CapAdd []string `json:"CapAdd"`
|
||||
CapDrop []string `json:"CapDrop"`
|
||||
Binds []string `json:"Binds"`
|
||||
Privileged bool `json:"Privileged"`
|
||||
PidMode string `json:"PidMode"`
|
||||
Devices []any `json:"Devices"`
|
||||
Sysctls map[string]any `json:"Sysctls"`
|
||||
SecurityOpt []string `json:"SecurityOpt"`
|
||||
CapAdd []string `json:"CapAdd"`
|
||||
CapDrop []string `json:"CapDrop"`
|
||||
Binds []string `json:"Binds"`
|
||||
Mounts []struct {
|
||||
Type string `json:"Type"`
|
||||
} `json:"Mounts"`
|
||||
} `json:"HostConfig"`
|
||||
}
|
||||
|
||||
forbiddenResponse := &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: http.NoBody,
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
@@ -230,7 +235,7 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
||||
return nil, ErrContainerCapabilitiesForbidden
|
||||
}
|
||||
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && len(partialContainer.HostConfig.Binds) > 0 {
|
||||
for _, bind := range partialContainer.HostConfig.Binds {
|
||||
if strings.HasPrefix(bind, "/") {
|
||||
return forbiddenResponse, ErrBindMountsForbidden
|
||||
@@ -238,6 +243,14 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
||||
}
|
||||
}
|
||||
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && len(partialContainer.HostConfig.Mounts) > 0 {
|
||||
for _, mount := range partialContainer.HostConfig.Mounts {
|
||||
if mount.Type == "bind" {
|
||||
return forbiddenResponse, ErrBindMountsForbidden
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
|
||||
@@ -252,3 +265,45 @@ func (transport *Transport) decorateContainerCreationOperation(request *http.Req
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateContainerUpdateOperation(request *http.Request, containerID string) (*http.Response, error) {
|
||||
type PartialContainerUpdate struct {
|
||||
Devices []any `json:"Devices"`
|
||||
}
|
||||
|
||||
forbiddenResponse := &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
}
|
||||
|
||||
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if isAdminOrEndpointAdmin {
|
||||
return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
|
||||
}
|
||||
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
partialUpdate := &PartialContainerUpdate{}
|
||||
if err := json.Unmarshal(body, partialUpdate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialUpdate.Devices) > 0 {
|
||||
return forbiddenResponse, ErrDeviceMappingForbidden
|
||||
}
|
||||
|
||||
request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/segmentio/encoding/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDecorateContainerCreationOperation_BindMounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
admin := portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
|
||||
regularUser := portainer.User{ID: 2, Username: "user", Role: portainer.StandardUserRole}
|
||||
|
||||
_, ds := datastore.MustNewTestStore(t, true, false)
|
||||
|
||||
err := ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.User().Create(&admin)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.User().Create(®ularUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.Endpoint().Create(&portainer.Endpoint{
|
||||
ID: 1,
|
||||
Name: "test",
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowBindMountsForRegularUsers: false,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
srv, version := mockDockerAPIServer(t, RoutesDefinition{
|
||||
{http.MethodPost, "/containers/create"}: map[string]any{"Id": "abc123", "Warnings": []any{}},
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
transport := &Transport{
|
||||
endpoint: &portainer.Endpoint{ID: 1, URL: srv.URL},
|
||||
dataStore: ds,
|
||||
HTTPTransport: &http.Transport{},
|
||||
}
|
||||
|
||||
adminToken := portainer.TokenData{ID: admin.ID, Username: admin.Username, Role: admin.Role}
|
||||
userToken := portainer.TokenData{ID: regularUser.ID, Username: regularUser.Username, Role: regularUser.Role}
|
||||
|
||||
makeRequest := func(token portainer.TokenData, body any) *http.Request {
|
||||
bodyBytes, err := json.Marshal(body)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, srv.URL+"/v"+version+"/containers/create", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(security.StoreTokenData(req, &token))
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// Admin bypasses security checks
|
||||
req := makeRequest(adminToken, map[string]any{
|
||||
"HostConfig": map[string]any{
|
||||
"Mounts": []map[string]any{{"Type": "bind", "Source": "/", "Target": "/host"}},
|
||||
},
|
||||
})
|
||||
resp, err := transport.decorateContainerCreationOperation(req, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// HostConfig.Binds with an absolute path is blocked for regular users
|
||||
req = makeRequest(userToken, map[string]any{
|
||||
"HostConfig": map[string]any{
|
||||
"Binds": []string{"/:/host:ro"},
|
||||
},
|
||||
})
|
||||
resp, err = transport.decorateContainerCreationOperation(req, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||
require.ErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// HostConfig.Mounts with type bind is blocked for regular users
|
||||
req = makeRequest(userToken, map[string]any{
|
||||
"HostConfig": map[string]any{
|
||||
"Mounts": []map[string]any{{"Type": "bind", "Source": "/", "Target": "/host"}},
|
||||
},
|
||||
})
|
||||
resp, err = transport.decorateContainerCreationOperation(req, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||
require.ErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
// HostConfig.Mounts with a non-bind type is allowed for regular users
|
||||
req = makeRequest(userToken, map[string]any{
|
||||
"HostConfig": map[string]any{
|
||||
"Mounts": []map[string]any{{"Type": "volume", "Source": "myvolume", "Target": "/data"}},
|
||||
},
|
||||
})
|
||||
resp, err = transport.decorateContainerCreationOperation(req, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -3,13 +3,13 @@ package docker
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/utils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/client"
|
||||
@@ -18,6 +18,70 @@ import (
|
||||
|
||||
const serviceObjectIdentifier = "ID"
|
||||
|
||||
type partialServiceSpec struct {
|
||||
TaskTemplate struct {
|
||||
ContainerSpec struct {
|
||||
CapabilityAdd []string `json:"CapabilityAdd"`
|
||||
CapabilityDrop []string `json:"CapabilityDrop"`
|
||||
Sysctls map[string]any `json:"Sysctls"`
|
||||
Privileges *struct {
|
||||
Seccomp *struct{ Mode string } `json:"Seccomp"`
|
||||
AppArmor *struct{ Mode string } `json:"AppArmor"`
|
||||
} `json:"Privileges"`
|
||||
Mounts []struct {
|
||||
Type string `json:"Type"`
|
||||
VolumeOptions *struct {
|
||||
DriverConfig *struct {
|
||||
Options map[string]string `json:"Options"`
|
||||
} `json:"DriverConfig"`
|
||||
} `json:"VolumeOptions"`
|
||||
} `json:"Mounts"`
|
||||
} `json:"ContainerSpec"`
|
||||
} `json:"TaskTemplate"`
|
||||
}
|
||||
|
||||
func CheckServiceBodyRestrictions(request *http.Request, securitySettings *portainer.EndpointSecuritySettings) error {
|
||||
defer logs.CloseAndLogErr(request.Body)
|
||||
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spec := &partialServiceSpec{}
|
||||
if err := json.Unmarshal(body, spec); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containerSpec := spec.TaskTemplate.ContainerSpec
|
||||
|
||||
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(containerSpec.CapabilityAdd) > 0 || len(containerSpec.CapabilityDrop) > 0) {
|
||||
return ErrContainerCapabilitiesForbidden
|
||||
}
|
||||
|
||||
if !securitySettings.AllowSysctlSettingForRegularUsers && len(containerSpec.Sysctls) > 0 {
|
||||
return ErrSysCtlSettingsForbidden
|
||||
}
|
||||
|
||||
if !securitySettings.AllowBindMountsForRegularUsers {
|
||||
for _, mount := range containerSpec.Mounts {
|
||||
if mount.Type == "bind" {
|
||||
return ErrBindMountsForbidden
|
||||
}
|
||||
|
||||
if mount.VolumeOptions != nil && mount.VolumeOptions.DriverConfig != nil {
|
||||
if mount.VolumeOptions.DriverConfig.Options["type"] == "bind" {
|
||||
return ErrBindMountsForbidden
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, endpointID portainer.EndpointID, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||
service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, swarm.ServiceInspectOptions{})
|
||||
if err != nil {
|
||||
@@ -90,20 +154,6 @@ func selectorServiceLabels(responseObject map[string]any) map[string]any {
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateServiceCreationOperation(request *http.Request) (*http.Response, error) {
|
||||
type PartialService struct {
|
||||
TaskTemplate struct {
|
||||
ContainerSpec struct {
|
||||
Mounts []struct {
|
||||
Type string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
forbiddenResponse := &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
}
|
||||
|
||||
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -118,25 +168,45 @@ func (transport *Transport) decorateServiceCreationOperation(request *http.Reque
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err := CheckServiceBodyRestrictions(request, securitySettings); err != nil {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: io.NopCloser(bytes.NewBufferString("Access denied: insufficient permissions to create service with specified configuration")),
|
||||
}, err
|
||||
}
|
||||
|
||||
return transport.replaceRegistryAuthenticationHeader(request)
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateServiceUpdateOperation(request *http.Request, serviceID string) (*http.Response, error) {
|
||||
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
partialService := &PartialService{}
|
||||
if err := json.Unmarshal(body, partialService); err != nil {
|
||||
if isAdminOrEndpointAdmin {
|
||||
if err := transport.decorateRegistryAuthenticationHeader(request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
|
||||
for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts {
|
||||
if mount.Type == "bind" {
|
||||
return forbiddenResponse, errors.New("forbidden to use bind mounts")
|
||||
}
|
||||
}
|
||||
if err := CheckServiceBodyRestrictions(request, securitySettings); err != nil {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: io.NopCloser(bytes.NewBufferString("Access denied: insufficient permissions to update service with specified configuration")),
|
||||
}, err
|
||||
}
|
||||
|
||||
request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
if err := transport.decorateRegistryAuthenticationHeader(request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return transport.replaceRegistryAuthenticationHeader(request)
|
||||
return transport.restrictedResourceOperation(request, serviceID, serviceID, portainer.ServiceResourceControl, false)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/segmentio/encoding/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const serviceCreationAPIVersion = "1.51"
|
||||
|
||||
type serviceCreationFixtures struct {
|
||||
dockerSrv *httptest.Server
|
||||
ds dataservices.DataStore
|
||||
stdUser portainer.User
|
||||
adminUser portainer.User
|
||||
endpointID portainer.EndpointID
|
||||
}
|
||||
|
||||
func newServiceCreationFixtures(t *testing.T) *serviceCreationFixtures {
|
||||
t.Helper()
|
||||
|
||||
const serviceID = "some-service-id"
|
||||
|
||||
dockerSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodHead && r.URL.Path == "/_ping" {
|
||||
w.Header().Add("Api-Version", serviceCreationAPIVersion)
|
||||
_, _ = w.Write([]byte{})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
data, err := json.Marshal(map[string]string{"ID": serviceID})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write(data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
t.Cleanup(dockerSrv.Close)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, false)
|
||||
|
||||
f := &serviceCreationFixtures{
|
||||
dockerSrv: dockerSrv,
|
||||
ds: store,
|
||||
stdUser: portainer.User{ID: 1, Username: "std", Role: portainer.StandardUserRole},
|
||||
adminUser: portainer.User{ID: 2, Username: "admin", Role: portainer.AdministratorRole},
|
||||
endpointID: portainer.EndpointID(1),
|
||||
}
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.User().Create(&f.stdUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.User().Create(&f.adminUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.Endpoint().Create(&portainer.Endpoint{ID: f.endpointID, Name: "test-env"})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *serviceCreationFixtures) setSecuritySettings(t *testing.T, settings portainer.EndpointSecuritySettings) {
|
||||
t.Helper()
|
||||
|
||||
err := f.ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return tx.Endpoint().UpdateEndpoint(f.endpointID, &portainer.Endpoint{
|
||||
ID: f.endpointID,
|
||||
Name: "test-env",
|
||||
SecuritySettings: settings,
|
||||
})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (f *serviceCreationFixtures) newTransport() *Transport {
|
||||
return &Transport{
|
||||
endpoint: &portainer.Endpoint{ID: f.endpointID},
|
||||
dataStore: f.ds,
|
||||
HTTPTransport: &http.Transport{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *serviceCreationFixtures) newRequest(t *testing.T, spec swarm.ServiceSpec, user portainer.User) *http.Request {
|
||||
t.Helper()
|
||||
|
||||
data, err := json.Marshal(spec)
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
t.Context(),
|
||||
http.MethodPost,
|
||||
f.dockerSrv.URL+"/v"+serviceCreationAPIVersion+"/services/create",
|
||||
bytes.NewReader(data),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
}))
|
||||
}
|
||||
|
||||
var (
|
||||
restrictiveSettings = portainer.EndpointSecuritySettings{
|
||||
AllowContainerCapabilitiesForRegularUsers: false,
|
||||
AllowSysctlSettingForRegularUsers: false,
|
||||
AllowBindMountsForRegularUsers: false,
|
||||
}
|
||||
|
||||
permissiveSettings = portainer.EndpointSecuritySettings{
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
}
|
||||
)
|
||||
|
||||
func TestDecorateServiceCreationOperation_CapabilityAddForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, restrictiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
CapabilityAdd: []string{"NET_ADMIN"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.ErrorIs(t, err, ErrContainerCapabilitiesForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_CapabilityDropForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, restrictiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
CapabilityDrop: []string{"MKNOD"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.ErrorIs(t, err, ErrContainerCapabilitiesForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_CapabilitiesAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, permissiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
CapabilityAdd: []string{"NET_ADMIN"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_NoCapabilitiesAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, restrictiveSettings)
|
||||
|
||||
var spec swarm.ServiceSpec
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.NotErrorIs(t, err, ErrContainerCapabilitiesForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_SysctlForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, restrictiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Sysctls: map[string]string{"net.ipv4.ip_forward": "1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.ErrorIs(t, err, ErrSysCtlSettingsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_SysctlAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, permissiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Sysctls: map[string]string{"net.ipv4.ip_forward": "1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_BindMountForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, restrictiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Mounts: []mount.Mount{{Type: mount.TypeBind}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.ErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_NonBindMountNotForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
|
||||
f.setSecuritySettings(t, portainer.EndpointSecuritySettings{
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowSysctlSettingForRegularUsers: true,
|
||||
AllowBindMountsForRegularUsers: false,
|
||||
})
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Mounts: []mount.Mount{{Type: mount.TypeVolume}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.NotErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_BindMountAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, permissiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Mounts: []mount.Mount{{Type: mount.TypeBind}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_AdminBypassesAllSecurityChecks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, restrictiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
CapabilityAdd: []string{"NET_ADMIN"},
|
||||
CapabilityDrop: []string{"MKNOD"},
|
||||
Sysctls: map[string]string{"net.ipv4.ip_forward": "1"},
|
||||
Mounts: []mount.Mount{{Type: mount.TypeBind}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.adminUser))
|
||||
require.NotErrorIs(t, err, ErrContainerCapabilitiesForbidden)
|
||||
require.NotErrorIs(t, err, ErrSysCtlSettingsForbidden)
|
||||
require.NotErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_StandardUserPermissiveSettingsSucceeds(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, permissiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
CapabilityAdd: []string{"NET_ADMIN"},
|
||||
Sysctls: map[string]string{"net.core.somaxconn": "128"},
|
||||
Mounts: []mount.Mount{{Type: mount.TypeBind}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_VolumeWithBindDriverOptionForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, restrictiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Mounts: []mount.Mount{{
|
||||
Type: mount.TypeVolume,
|
||||
VolumeOptions: &mount.VolumeOptions{
|
||||
DriverConfig: &mount.Driver{
|
||||
Options: map[string]string{"type": "bind", "device": "/etc"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.ErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_VolumeWithBindDriverOptionAllowed(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, permissiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Mounts: []mount.Mount{{
|
||||
Type: mount.TypeVolume,
|
||||
VolumeOptions: &mount.VolumeOptions{
|
||||
DriverConfig: &mount.Driver{
|
||||
Options: map[string]string{"type": "bind", "device": "/etc"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceCreationOperation_VolumeWithNonBindDriverOptionNotForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, restrictiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Mounts: []mount.Mount{{
|
||||
Type: mount.TypeVolume,
|
||||
VolumeOptions: &mount.VolumeOptions{
|
||||
DriverConfig: &mount.Driver{
|
||||
Options: map[string]string{"type": "tmpfs"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceCreationOperation(f.newRequest(t, spec, f.stdUser))
|
||||
require.NotErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateServiceUpdateOperation_VolumeWithBindDriverOptionForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newServiceCreationFixtures(t)
|
||||
f.setSecuritySettings(t, restrictiveSettings)
|
||||
|
||||
spec := swarm.ServiceSpec{
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Mounts: []mount.Mount{{
|
||||
Type: mount.TypeVolume,
|
||||
VolumeOptions: &mount.VolumeOptions{
|
||||
DriverConfig: &mount.Driver{
|
||||
Options: map[string]string{"type": "bind", "device": "/etc"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateServiceUpdateOperation(f.newRequest(t, spec, f.stdUser), "test-service-id")
|
||||
require.ErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
"github.com/portainer/portainer/api/slicesx"
|
||||
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
@@ -109,6 +110,28 @@ var prefixProxyFuncMap = map[string]func(*Transport, *http.Request, string) (*ht
|
||||
"volumes": (*Transport).proxyVolumeRequest,
|
||||
}
|
||||
|
||||
type route struct {
|
||||
method string
|
||||
pattern *regexp.Regexp
|
||||
}
|
||||
|
||||
var adminOnlyRoutes = []route{
|
||||
{http.MethodPost, regexp.MustCompile(`^/plugins/.+/enable$`)},
|
||||
{http.MethodPost, regexp.MustCompile(`^/plugins/.+/disable$`)},
|
||||
{http.MethodPost, regexp.MustCompile(`^/plugins/pull$`)},
|
||||
{http.MethodPost, regexp.MustCompile(`^/plugins/.+/push$`)},
|
||||
{http.MethodPost, regexp.MustCompile(`^/plugins/.+/upgrade$`)},
|
||||
{http.MethodPost, regexp.MustCompile(`^/plugins/.+/set$`)},
|
||||
{http.MethodPost, regexp.MustCompile(`^/plugins/create$`)},
|
||||
{http.MethodDelete, regexp.MustCompile(`^/plugins/.+$`)},
|
||||
}
|
||||
|
||||
func isAdminOnlyRoute(method string, path string) bool {
|
||||
return slicesx.Some(adminOnlyRoutes, func(r route) bool {
|
||||
return method == r.method && r.pattern.MatchString(path)
|
||||
})
|
||||
}
|
||||
|
||||
// ProxyDockerRequest intercepts a Docker API request and apply logic based
|
||||
// on the requested operation.
|
||||
func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||
@@ -137,6 +160,10 @@ func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Res
|
||||
return proxyFunc(transport, request, unversionedPath)
|
||||
}
|
||||
|
||||
if isAdminOnlyRoute(request.Method, unversionedPath) {
|
||||
return transport.administratorOperation(request)
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
@@ -261,6 +288,11 @@ func (transport *Transport) proxyContainerRequest(request *http.Request, unversi
|
||||
if action == "json" {
|
||||
return transport.rewriteOperation(request, transport.containerInspectOperation)
|
||||
}
|
||||
|
||||
if action == "update" {
|
||||
return transport.decorateContainerUpdateOperation(request, containerID)
|
||||
}
|
||||
|
||||
return transport.restrictedResourceOperation(request, containerID, containerID, portainer.ContainerResourceControl, false)
|
||||
} else if match, _ := path.Match("/containers/*", requestPath); match {
|
||||
// Handle /containers/{id} requests
|
||||
@@ -292,6 +324,11 @@ func (transport *Transport) proxyServiceRequest(request *http.Request, unversion
|
||||
if match, _ := path.Match("/services/*/*", requestPath); match {
|
||||
// Handle /services/{id}/{action} requests
|
||||
serviceID := path.Base(path.Dir(requestPath))
|
||||
action := path.Base(requestPath)
|
||||
|
||||
if action == "update" {
|
||||
return transport.decorateServiceUpdateOperation(request, serviceID)
|
||||
}
|
||||
|
||||
if err := transport.decorateRegistryAuthenticationHeader(request); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -108,6 +108,141 @@ func mockDockerAPIServer(t *testing.T, routes RoutesDefinition) (*httptest.Serve
|
||||
return srv, version
|
||||
}
|
||||
|
||||
func TestTransport_adminProxy(t *testing.T) {
|
||||
t.Parallel()
|
||||
admin := portainer.User{ID: 1, Username: "admin", Role: portainer.AdministratorRole}
|
||||
std1 := portainer.User{ID: 2, Username: "std1", Role: portainer.StandardUserRole}
|
||||
std2 := portainer.User{ID: 3, Username: "std2", Role: portainer.StandardUserRole}
|
||||
|
||||
_, ds := datastore.MustNewTestStore(t, true, false)
|
||||
|
||||
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
require.NoError(t, tx.User().Create(&admin))
|
||||
require.NoError(t, tx.User().Create(&std1))
|
||||
require.NoError(t, tx.User().Create(&std2))
|
||||
require.NoError(t, tx.Endpoint().Create(&portainer.Endpoint{ID: 1, Name: "env",
|
||||
UserAccessPolicies: portainer.UserAccessPolicies{std1.ID: portainer.AccessPolicy{RoleID: 1}},
|
||||
}))
|
||||
|
||||
return nil
|
||||
}))
|
||||
srv, version := mockDockerAPIServer(t, RoutesDefinition{
|
||||
// allowed routes
|
||||
{http.MethodGet, "/plugins"}: nil,
|
||||
{http.MethodGet, "/plugins/xxx/json"}: nil,
|
||||
{http.MethodGet, "/plugins/privileges"}: nil,
|
||||
// admin routes ; see `adminOnlyRoutes`
|
||||
{http.MethodDelete, "/plugins/xxx"}: nil,
|
||||
{http.MethodPost, "/plugins/sshfs/enable"}: nil, // simulate plugin "sshfs"
|
||||
{http.MethodPost, "/plugins/vieux/sshfs/enable"}: nil, // simulate "vieux/sshfs"
|
||||
{http.MethodPost, "/plugins/xxx/disable"}: nil,
|
||||
{http.MethodPost, "/plugins/pull"}: nil,
|
||||
{http.MethodPost, "/plugins/xxx/push"}: nil,
|
||||
{http.MethodPost, "/plugins/xxx/upgrade"}: nil,
|
||||
{http.MethodPost, "/plugins/xxx/set"}: nil,
|
||||
{http.MethodPost, "/plugins/create"}: nil,
|
||||
})
|
||||
defer srv.Close()
|
||||
|
||||
transport := &Transport{
|
||||
endpoint: &portainer.Endpoint{URL: srv.URL},
|
||||
dataStore: ds,
|
||||
HTTPTransport: &http.Transport{},
|
||||
}
|
||||
|
||||
test := func(method string, url string, token portainer.TokenData) (*http.Response, error) {
|
||||
req := httptest.NewRequest(method, srv.URL+"/v"+version+url, nil)
|
||||
req = req.WithContext(security.StoreTokenData(req, &token))
|
||||
require.NotNil(t, req)
|
||||
|
||||
return transport.ProxyDockerRequest(req)
|
||||
}
|
||||
|
||||
adminToken := portainer.TokenData{ID: admin.ID, Username: admin.Username, Role: admin.Role}
|
||||
std1Token := portainer.TokenData{ID: std1.ID, Username: std1.Username, Role: std1.Role}
|
||||
std2Token := portainer.TokenData{ID: std2.ID, Username: std2.Username, Role: std2.Role}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodGet, "/plugins", adminToken)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodGet, "/plugins", std1Token)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodGet, "/plugins", std2Token)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodPost, "/plugins/pull", adminToken)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodPost, "/plugins/pull", std1Token)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusForbidden, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodPost, "/plugins/pull", std2Token)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusForbidden, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodPost, "/plugins/sshfs/enable", adminToken)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodPost, "/plugins/sshfs/enable", std2Token)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusForbidden, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodPost, "/plugins/vieux/sshfs/enable", adminToken)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusOK, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
|
||||
{
|
||||
r, err := test(http.MethodPost, "/plugins/vieux/sshfs/enable", std2Token)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, r)
|
||||
require.Equal(t, http.StatusForbidden, r.StatusCode)
|
||||
require.NoError(t, r.Body.Close())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransport_getRealResourceID(t *testing.T) {
|
||||
srv, _ := mockDockerAPIServer(t, RoutesDefinition{
|
||||
{http.MethodGet, "/networks"}: []network.Summary{{ID: "16e37c629e88694663791dc738fd37affb908d7b85ce00a20680675d10554fd4", Name: "mynetwork"}},
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
@@ -15,6 +17,7 @@ import (
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
const volumeObjectIdentifier = "ResourceID"
|
||||
@@ -122,12 +125,58 @@ func selectorVolumeLabels(responseObject map[string]any) map[string]any {
|
||||
return utils.GetJSONObject(responseObject, "Labels")
|
||||
}
|
||||
|
||||
func CheckVolumeBodyRestrictions(request *http.Request) error {
|
||||
defer logs.CloseAndLogErr(request.Body)
|
||||
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var volumeCreateBody struct {
|
||||
DriverOpts map[string]string `json:"DriverOpts"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &volumeCreateBody); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if volumeCreateBody.DriverOpts["type"] == "bind" {
|
||||
return ErrBindMountsForbidden
|
||||
}
|
||||
|
||||
request.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateVolumeResourceCreationOperation(request *http.Request, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isAdminOrEndpointAdmin {
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !securitySettings.AllowBindMountsForRegularUsers {
|
||||
if err := CheckVolumeBodyRestrictions(request); err != nil {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
Body: io.NopCloser(bytes.NewBufferString("Access denied: insufficient permissions to create volume with specified configuration")),
|
||||
}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volumeID := request.Header.Get("X-Portainer-VolumeName")
|
||||
|
||||
if volumeID != "" {
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/segmentio/encoding/json"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const volumeCreationAPIVersion = "1.51"
|
||||
|
||||
type volumeCreationFixtures struct {
|
||||
dockerSrv *httptest.Server
|
||||
ds dataservices.DataStore
|
||||
stdUser portainer.User
|
||||
adminUser portainer.User
|
||||
endpointID portainer.EndpointID
|
||||
}
|
||||
|
||||
func newVolumeCreationFixtures(t *testing.T) *volumeCreationFixtures {
|
||||
t.Helper()
|
||||
|
||||
dockerSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodHead && r.URL.Path == "/_ping" {
|
||||
w.Header().Add("Api-Version", volumeCreationAPIVersion)
|
||||
_, _ = w.Write([]byte{})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
data, err := json.Marshal(map[string]string{"Name": "test-volume"})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write(data)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
t.Cleanup(dockerSrv.Close)
|
||||
|
||||
_, store := datastore.MustNewTestStore(t, true, false)
|
||||
|
||||
f := &volumeCreationFixtures{
|
||||
dockerSrv: dockerSrv,
|
||||
ds: store,
|
||||
stdUser: portainer.User{ID: 1, Username: "std", Role: portainer.StandardUserRole},
|
||||
adminUser: portainer.User{ID: 2, Username: "admin", Role: portainer.AdministratorRole},
|
||||
endpointID: portainer.EndpointID(1),
|
||||
}
|
||||
|
||||
err := store.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
err := tx.User().Create(&f.stdUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.User().Create(&f.adminUser)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = tx.Endpoint().Create(&portainer.Endpoint{ID: f.endpointID, Name: "test-env"})
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *volumeCreationFixtures) setSecuritySettings(t *testing.T, settings portainer.EndpointSecuritySettings) {
|
||||
t.Helper()
|
||||
|
||||
err := f.ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return tx.Endpoint().UpdateEndpoint(f.endpointID, &portainer.Endpoint{
|
||||
ID: f.endpointID,
|
||||
Name: "test-env",
|
||||
SecuritySettings: settings,
|
||||
})
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (f *volumeCreationFixtures) newTransport() *Transport {
|
||||
return &Transport{
|
||||
endpoint: &portainer.Endpoint{ID: f.endpointID},
|
||||
dataStore: f.ds,
|
||||
HTTPTransport: &http.Transport{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *volumeCreationFixtures) newRequest(t *testing.T, body volume.CreateOptions, user portainer.User) *http.Request {
|
||||
t.Helper()
|
||||
|
||||
data, err := json.Marshal(body)
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequestWithContext(
|
||||
t.Context(),
|
||||
http.MethodPost,
|
||||
f.dockerSrv.URL+"/v"+volumeCreationAPIVersion+"/volumes/create",
|
||||
bytes.NewReader(data),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return req.WithContext(security.StoreTokenData(req, &portainer.TokenData{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
}))
|
||||
}
|
||||
|
||||
func TestDecorateVolumeResourceCreationOperation_BindDriverOptForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newVolumeCreationFixtures(t)
|
||||
f.setSecuritySettings(t, portainer.EndpointSecuritySettings{
|
||||
AllowBindMountsForRegularUsers: false,
|
||||
})
|
||||
|
||||
body := volume.CreateOptions{
|
||||
Name: "evil-volume",
|
||||
Driver: "local",
|
||||
DriverOpts: map[string]string{
|
||||
"type": "bind",
|
||||
"device": "/etc",
|
||||
"o": "bind",
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateVolumeResourceCreationOperation(f.newRequest(t, body, f.stdUser), portainer.VolumeResourceControl)
|
||||
require.ErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateVolumeResourceCreationOperation_BindDriverOptAllowedForAdmin(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newVolumeCreationFixtures(t)
|
||||
f.setSecuritySettings(t, portainer.EndpointSecuritySettings{
|
||||
AllowBindMountsForRegularUsers: false,
|
||||
})
|
||||
|
||||
body := volume.CreateOptions{
|
||||
Name: "admin-volume",
|
||||
Driver: "local",
|
||||
DriverOpts: map[string]string{
|
||||
"type": "bind",
|
||||
"device": "/etc",
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateVolumeResourceCreationOperation(f.newRequest(t, body, f.adminUser), portainer.VolumeResourceControl)
|
||||
require.NotErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateVolumeResourceCreationOperation_BindDriverOptAllowedWhenSettingPermissive(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newVolumeCreationFixtures(t)
|
||||
f.setSecuritySettings(t, portainer.EndpointSecuritySettings{
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
})
|
||||
|
||||
body := volume.CreateOptions{
|
||||
Name: "allowed-volume",
|
||||
Driver: "local",
|
||||
DriverOpts: map[string]string{
|
||||
"type": "bind",
|
||||
"device": "/data",
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateVolumeResourceCreationOperation(f.newRequest(t, body, f.stdUser), portainer.VolumeResourceControl)
|
||||
require.NotErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestDecorateVolumeResourceCreationOperation_NonBindDriverOptNotForbidden(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f := newVolumeCreationFixtures(t)
|
||||
f.setSecuritySettings(t, portainer.EndpointSecuritySettings{
|
||||
AllowBindMountsForRegularUsers: false,
|
||||
})
|
||||
|
||||
body := volume.CreateOptions{
|
||||
Name: "normal-volume",
|
||||
Driver: "local",
|
||||
DriverOpts: map[string]string{
|
||||
"type": "tmpfs",
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := f.newTransport().decorateVolumeResourceCreationOperation(f.newRequest(t, body, f.stdUser), portainer.VolumeResourceControl)
|
||||
require.NotErrorIs(t, err, ErrBindMountsForbidden)
|
||||
require.NotNil(t, resp)
|
||||
require.NotEqual(t, http.StatusForbidden, resp.StatusCode)
|
||||
err = resp.Body.Close()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -447,26 +447,14 @@ func (bouncer *RequestBouncer) apiKeyLookup(r *http.Request) (*portainer.TokenDa
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
// extractBearerToken extracts the Bearer token from the request header or query parameter and returns the token.
|
||||
// extractBearerToken extracts the Bearer token from the Authorization header and returns the token.
|
||||
func extractBearerToken(r *http.Request) (string, bool) {
|
||||
// Token might be set via the "token" query parameter.
|
||||
// For example, in websocket requests
|
||||
// For these cases, hide the token from the query
|
||||
query := r.URL.Query()
|
||||
token := query.Get("token")
|
||||
if token != "" {
|
||||
query.Del("token")
|
||||
r.URL.RawQuery = query.Encode()
|
||||
|
||||
return token, true
|
||||
}
|
||||
|
||||
tokens, ok := r.Header[jwtTokenHeader]
|
||||
if !ok || len(tokens) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
token = tokens[0]
|
||||
token := tokens[0]
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
|
||||
return token, true
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package registryutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
@@ -14,9 +16,12 @@ func isRegTokenValid(registry *portainer.Registry) (valid bool) {
|
||||
return registry.AccessToken != "" && registry.AccessTokenExpiry > time.Now().Unix()
|
||||
}
|
||||
|
||||
func doGetRegToken(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
|
||||
func fetchRegToken(registry *portainer.Registry) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
ecrClient := ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region)
|
||||
accessToken, expiryAt, err := ecrClient.GetAuthorizationToken()
|
||||
accessToken, expiryAt, err := ecrClient.GetAuthorizationToken(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -24,12 +29,34 @@ func doGetRegToken(tx dataservices.DataStoreTx, registry *portainer.Registry) er
|
||||
registry.AccessToken = *accessToken
|
||||
registry.AccessTokenExpiry = expiryAt.Unix()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func doGetRegToken(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
|
||||
if err := fetchRegToken(registry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Registry().Update(registry.ID, registry)
|
||||
}
|
||||
|
||||
func parseRegToken(registry *portainer.Registry) (username, password string, err error) {
|
||||
return ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region).
|
||||
ParseAuthorizationToken(registry.AccessToken)
|
||||
// ValidateRegistriesECRTokens refreshes and persists ECR tokens for all registries that need it.
|
||||
// Must be called with a real DataStoreTx (not a top-level DataStore) to avoid write-lock contention.
|
||||
func ValidateRegistriesECRTokens(tx dataservices.DataStoreTx, registries []portainer.Registry) error {
|
||||
for i := range registries {
|
||||
reg := ®istries[i]
|
||||
if reg.Type != portainer.EcrRegistry {
|
||||
continue
|
||||
}
|
||||
if isRegTokenValid(reg) {
|
||||
continue
|
||||
}
|
||||
if err := doGetRegToken(tx, reg); err != nil {
|
||||
return fmt.Errorf("ECR registry %q credentials are invalid or expired. Error: %w", reg.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureRegTokenValid(tx dataservices.DataStoreTx, registry *portainer.Registry) error {
|
||||
@@ -57,7 +84,15 @@ func GetRegEffectiveCredential(registry *portainer.Registry) (username, password
|
||||
password = registry.Password
|
||||
|
||||
if registry.Type == portainer.EcrRegistry {
|
||||
username, password, err = parseRegToken(registry)
|
||||
// Fallback token refresh in case the upstream caller did not pre-validate the token.
|
||||
if !isRegTokenValid(registry) {
|
||||
if err := fetchRegToken(registry); err != nil {
|
||||
return "", "", fmt.Errorf("ECR registry %q credentials are invalid or expired. Error: %w", registry.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
username, password, err = ecr.NewService(registry.Username, registry.Password, registry.Ecr.Region).
|
||||
ParseAuthorizationToken(registry.AccessToken)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package registryutils_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/datastore"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newECRRegistry(id portainer.RegistryID, accessToken string, expiry int64) portainer.Registry {
|
||||
return portainer.Registry{
|
||||
ID: id,
|
||||
Type: portainer.EcrRegistry,
|
||||
Name: "test-ecr",
|
||||
Username: "AKIAIOSFODNN7EXAMPLE",
|
||||
Password: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
Ecr: portainer.EcrData{Region: "us-east-1"},
|
||||
AccessToken: accessToken,
|
||||
AccessTokenExpiry: expiry,
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRegistriesECRTokens(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("skips non-ECR registries without error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ds := datastore.MustNewTestStore(t, true, false)
|
||||
registries := []portainer.Registry{
|
||||
{ID: 1, Type: portainer.DockerHubRegistry, Name: "dockerhub"},
|
||||
{ID: 2, Type: portainer.CustomRegistry, Name: "custom"},
|
||||
}
|
||||
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return registryutils.ValidateRegistriesECRTokens(tx, registries)
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("skips ECR registries with valid tokens", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ds := datastore.MustNewTestStore(t, true, false)
|
||||
reg := newECRRegistry(1, "valid-token", time.Now().Add(time.Hour).Unix())
|
||||
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return registryutils.ValidateRegistriesECRTokens(tx, []portainer.Registry{reg})
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("returns nil for empty registry list", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ds := datastore.MustNewTestStore(t, true, false)
|
||||
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return registryutils.ValidateRegistriesECRTokens(tx, []portainer.Registry{})
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("returns error for ECR registry with missing token", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ds := datastore.MustNewTestStore(t, true, false)
|
||||
reg := newECRRegistry(1, "", 0)
|
||||
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return tx.Registry().Create(®)
|
||||
}))
|
||||
|
||||
var validateErr error
|
||||
_ = ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
validateErr = registryutils.ValidateRegistriesECRTokens(tx, []portainer.Registry{reg})
|
||||
return nil
|
||||
})
|
||||
require.Error(t, validateErr)
|
||||
require.Contains(t, validateErr.Error(), "test-ecr")
|
||||
})
|
||||
|
||||
t.Run("stops on first invalid ECR registry and includes its name in error", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, ds := datastore.MustNewTestStore(t, true, false)
|
||||
|
||||
validECR := newECRRegistry(1, "valid-token", time.Now().Add(time.Hour).Unix())
|
||||
invalidECR := newECRRegistry(2, "", 0)
|
||||
invalidECR.Name = "invalid-ecr"
|
||||
nonECR := portainer.Registry{ID: 3, Type: portainer.DockerHubRegistry}
|
||||
|
||||
require.NoError(t, ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
return tx.Registry().Create(&invalidECR)
|
||||
}))
|
||||
|
||||
var validateErr error
|
||||
_ = ds.UpdateTx(func(tx dataservices.DataStoreTx) error {
|
||||
validateErr = registryutils.ValidateRegistriesECRTokens(tx, []portainer.Registry{validECR, invalidECR, nonECR})
|
||||
return nil
|
||||
})
|
||||
require.Error(t, validateErr)
|
||||
require.Contains(t, validateErr.Error(), "invalid-ecr")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetRegEffectiveCredential(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("returns username and password directly for non-ECR registry", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
reg := &portainer.Registry{
|
||||
Type: portainer.DockerHubRegistry,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
}
|
||||
username, password, err := registryutils.GetRegEffectiveCredential(reg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "user", username)
|
||||
require.Equal(t, "pass", password)
|
||||
})
|
||||
|
||||
t.Run("parses ECR access token when token is valid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
reg := newECRRegistry(1, "AWS:ecr-password", time.Now().Add(time.Hour).Unix())
|
||||
username, password, err := registryutils.GetRegEffectiveCredential(®)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "AWS", username)
|
||||
require.Equal(t, "ecr-password", password)
|
||||
})
|
||||
|
||||
t.Run("returns error for ECR registry with missing token and invalid credentials", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
reg := newECRRegistry(1, "", 0)
|
||||
_, _, err := registryutils.GetRegEffectiveCredential(®)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "test-ecr")
|
||||
})
|
||||
}
|
||||
@@ -147,6 +147,27 @@ func WithSettingsService(settings *portainer.Settings) datastoreOption {
|
||||
}
|
||||
}
|
||||
|
||||
type stubSSLSettingsService struct {
|
||||
settings *portainer.SSLSettings
|
||||
}
|
||||
|
||||
func (s *stubSSLSettingsService) BucketName() string { return "ssl" }
|
||||
|
||||
func (s *stubSSLSettingsService) Settings() (*portainer.SSLSettings, error) {
|
||||
return s.settings, nil
|
||||
}
|
||||
|
||||
func (s *stubSSLSettingsService) UpdateSettings(settings *portainer.SSLSettings) error {
|
||||
s.settings = settings
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithSSLSettingsService(settings *portainer.SSLSettings) datastoreOption {
|
||||
return func(d *testDatastore) {
|
||||
d.sslSettings = &stubSSLSettingsService{settings: settings}
|
||||
}
|
||||
}
|
||||
|
||||
type stubUserService struct {
|
||||
dataservices.UserService
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ func (kcl *KubeClient) fetchCronJobs(namespace string) ([]models.K8sCronJob, err
|
||||
|
||||
// parseCronJob converts a batchv1.CronJob object to a models.K8sCronJob object.
|
||||
func (kcl *KubeClient) parseCronJob(cronJob batchv1.CronJob, jobsList *batchv1.JobList) models.K8sCronJob {
|
||||
jobs, err := kcl.getCronJobExecutions(cronJob.Name, jobsList)
|
||||
jobs, err := kcl.getCronJobExecutions(cronJob.Name, cronJob.Namespace, jobsList)
|
||||
if err != nil {
|
||||
return models.K8sCronJob{}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import (
|
||||
"testing"
|
||||
|
||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
batchv1 "k8s.io/api/batch/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
kfake "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
@@ -64,3 +67,62 @@ func (kcl *KubeClient) TestFetchCronJobs(t *testing.T) {
|
||||
t.Logf("Deleted Cron Jobs")
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetCronJobExecutionsNamespaceFilter verifies that getCronJobExecutions only returns
|
||||
// executions belonging to the CronJob's own namespace, even when same-named CronJobs
|
||||
// exist across multiple namespaces.
|
||||
func TestGetCronJobExecutionsNamespaceFilter(t *testing.T) {
|
||||
backoffLimit := int32(3)
|
||||
completions := int32(1)
|
||||
|
||||
makeJob := func(name, namespace, cronJobName string) batchv1.Job {
|
||||
return batchv1.Job{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{Kind: "CronJob", Name: cronJobName},
|
||||
},
|
||||
},
|
||||
Spec: batchv1.JobSpec{
|
||||
BackoffLimit: &backoffLimit,
|
||||
Completions: &completions,
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "worker", Image: "busybox"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Simulate the cross-namespace job list returned when fetchCronJobs is called with namespace=""
|
||||
allJobs := &batchv1.JobList{
|
||||
Items: []batchv1.Job{
|
||||
makeJob("backup-prod-28001440", "ns-prod", "backup"),
|
||||
makeJob("backup-test-28001441", "ns-test", "backup"),
|
||||
},
|
||||
}
|
||||
|
||||
kcl := &KubeClient{
|
||||
cli: kfake.NewSimpleClientset(),
|
||||
instanceID: "test",
|
||||
isKubeAdmin: true,
|
||||
}
|
||||
|
||||
t.Run("returns only executions from the matching namespace", func(t *testing.T) {
|
||||
result, err := kcl.getCronJobExecutions("backup", "ns-prod", allJobs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, "ns-prod", result[0].Namespace)
|
||||
assert.Equal(t, "backup-prod-28001440", result[0].Name)
|
||||
})
|
||||
|
||||
t.Run("returns only executions from the other matching namespace", func(t *testing.T) {
|
||||
result, err := kcl.getCronJobExecutions("backup", "ns-test", allJobs)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, "ns-test", result[0].Namespace)
|
||||
assert.Equal(t, "backup-test-28001441", result[0].Name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -168,11 +168,15 @@ func getJobPodName(kcl *KubeClient, job batchv1.Job) string {
|
||||
|
||||
// getCronJobExecutions returns the jobs for a given cronjob
|
||||
// it returns the jobs for the cronjob
|
||||
func (kcl *KubeClient) getCronJobExecutions(cronJobName string, jobs *batchv1.JobList) ([]models.K8sJob, error) {
|
||||
func (kcl *KubeClient) getCronJobExecutions(cronJobName string, cronJobNamespace string, jobs *batchv1.JobList) ([]models.K8sJob, error) {
|
||||
maxItems := 5
|
||||
|
||||
results := make([]models.K8sJob, 0)
|
||||
for _, job := range jobs.Items {
|
||||
if job.Namespace != cronJobNamespace {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, owner := range job.OwnerReferences {
|
||||
if owner.Kind == "CronJob" && owner.Name == cronJobName {
|
||||
results = append(results, kcl.parseJob(job))
|
||||
|
||||
+5
-5
@@ -73,7 +73,7 @@ func (Service) AuthenticateUser(username, password string, settings *portainer.L
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer connection.Close()
|
||||
defer func() { _ = connection.Close() }()
|
||||
|
||||
if !settings.AnonymousMode {
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
@@ -108,7 +108,7 @@ func (Service) GetUserGroups(username string, settings *portainer.LDAPSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer connection.Close()
|
||||
defer func() { _ = connection.Close() }()
|
||||
|
||||
if !settings.AnonymousMode {
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
@@ -133,7 +133,7 @@ func (Service) SearchUsers(settings *portainer.LDAPSettings) ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer connection.Close()
|
||||
defer func() { _ = connection.Close() }()
|
||||
|
||||
if !settings.AnonymousMode {
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
@@ -182,7 +182,7 @@ func (Service) SearchGroups(settings *portainer.LDAPSettings) ([]portainer.LDAPU
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer connection.Close()
|
||||
defer func() { _ = connection.Close() }()
|
||||
|
||||
if !settings.AnonymousMode {
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
@@ -309,7 +309,7 @@ func (Service) TestConnectivity(settings *portainer.LDAPSettings) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer connection.Close()
|
||||
defer func() { _ = connection.Close() }()
|
||||
|
||||
if !settings.AnonymousMode {
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestCreateConnectionForURL(t *testing.T) {
|
||||
conn, err := createConnectionForURL(settings.URL, settings)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, conn)
|
||||
conn.Close()
|
||||
require.NoError(t, conn.Close())
|
||||
|
||||
// TLS
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestCreateConnectionForURL(t *testing.T) {
|
||||
conn, err = createConnectionForURL(settings.URL, settings)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, conn)
|
||||
conn.Close()
|
||||
require.NoError(t, conn.Close())
|
||||
|
||||
// Invalid TLS
|
||||
|
||||
|
||||
+2
-1
@@ -141,6 +141,7 @@ type (
|
||||
LogLevel *string
|
||||
LogMode *string
|
||||
KubectlShellImage *string
|
||||
KubectlShellImageSet bool
|
||||
PullLimitCheckDisabled *bool
|
||||
TrustedOrigins *string
|
||||
}
|
||||
@@ -1874,7 +1875,7 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.39.1"
|
||||
APIVersion = "2.39.2"
|
||||
// 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
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -44,6 +45,10 @@ func CreateComposeStackDeploymentConfigTx(tx dataservices.DataStoreTx, securityC
|
||||
|
||||
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
|
||||
if err := registryutils.ValidateRegistriesECRTokens(tx, filteredRegistries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := &ComposeStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/registryutils"
|
||||
"github.com/portainer/portainer/api/stacks/stackutils"
|
||||
)
|
||||
|
||||
@@ -43,6 +44,10 @@ func CreateSwarmStackDeploymentConfigTx(tx dataservices.DataStoreTx, securityCon
|
||||
|
||||
filteredRegistries := security.FilterRegistries(registries, user, securityContext.UserMemberships, endpoint.ID)
|
||||
|
||||
if err := registryutils.ValidateRegistriesECRTokens(tx, filteredRegistries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
config := &SwarmStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export interface StandaloneFileContentPayload {
|
||||
|
||||
stackFileContent: string;
|
||||
/** List of environment variables */
|
||||
env?: Array<Pair>;
|
||||
env?: Array<Pair> | null;
|
||||
|
||||
/** Whether the stack is from an app template */
|
||||
fromAppTemplate?: boolean;
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface SwarmFileContentPayload {
|
||||
|
||||
stackFileContent: string;
|
||||
/** List of environment variables */
|
||||
env?: Array<Pair>;
|
||||
env?: Array<Pair> | null;
|
||||
|
||||
/** Whether the stack is from an app template */
|
||||
fromAppTemplate?: boolean;
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface Stack {
|
||||
EndpointId: number;
|
||||
SwarmId: string;
|
||||
EntryPoint: string;
|
||||
Env: EnvVar[];
|
||||
Env: EnvVar[] | null;
|
||||
ResourceControl?: ResourceControlResponse;
|
||||
Status: StackStatus;
|
||||
ProjectPath: string;
|
||||
@@ -84,7 +84,7 @@ export type StackFile = {
|
||||
};
|
||||
|
||||
export interface GitStackPayload {
|
||||
env: Array<EnvVar>;
|
||||
env: Array<EnvVar> | null;
|
||||
prune?: boolean;
|
||||
RepositoryReferenceName?: string;
|
||||
RepositoryAuthentication?: boolean;
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
.background-error {
|
||||
padding-top: 55px;
|
||||
background-image: url(~assets/images/icon-error.svg);
|
||||
background-image: url(~@/assets/images/icon-error.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top left;
|
||||
}
|
||||
|
||||
.background-warning {
|
||||
padding-top: 55px;
|
||||
background-image: url(~assets/images/icon-warning.svg);
|
||||
background-image: url(~@/assets/images/icon-warning.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: top left;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export function HealthStatus({ health }: Props) {
|
||||
<div className="vertical-center">{health.FailingStreak}</div>
|
||||
</DetailsTable.Row>
|
||||
|
||||
{!!health.Log && (
|
||||
{!!health.Log?.length && (
|
||||
<DetailsTable.Row label="Last output">
|
||||
{health.Log[health.Log.length - 1].Output}
|
||||
</DetailsTable.Row>
|
||||
|
||||
@@ -57,7 +57,7 @@ export function StackEditorTab({
|
||||
);
|
||||
|
||||
const initialValues: StackEditorFormValues = {
|
||||
environmentVariables: stack.Env,
|
||||
environmentVariables: stack.Env || [],
|
||||
prune: !!(stack.Option && stack.Option.Prune),
|
||||
stackFileContent: originalFileContent,
|
||||
enabledWebhook: !!stack.Webhook,
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ export async function duplicateStack({
|
||||
fileContent: string;
|
||||
targetEnvironmentId: EnvironmentId;
|
||||
type: StackType;
|
||||
env?: Array<Pair>;
|
||||
env?: Array<Pair> | null;
|
||||
}) {
|
||||
if (type === StackType.DockerSwarm) {
|
||||
const swarm = await getSwarm(targetEnvironmentId);
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export function StackRedeployGitForm({ stack }: { stack: Stack }) {
|
||||
SaveCredential: false,
|
||||
},
|
||||
autoUpdate: parseAutoUpdateResponse(stack.AutoUpdate),
|
||||
env: stack.Env,
|
||||
env: stack.Env || [],
|
||||
prune: stack.Option?.Prune || false,
|
||||
refName: stack.GitConfig?.ReferenceName || '',
|
||||
tlsSkipVerify: stack.GitConfig?.TLSSkipVerify || false,
|
||||
|
||||
@@ -15,7 +15,7 @@ export function useUpdateStackMutation() {
|
||||
|
||||
type Payload = {
|
||||
stackFileContent: string;
|
||||
env?: EnvVarValues;
|
||||
env?: EnvVarValues | null;
|
||||
prune?: boolean;
|
||||
webhook?: string;
|
||||
repullImageAndRedeploy?: boolean;
|
||||
|
||||
@@ -120,7 +120,7 @@ async function createStackAndGitCredential(
|
||||
autoUpdate: AutoUpdateResponse | null;
|
||||
}
|
||||
) {
|
||||
const newGitModel = await saveGitCredentialsIfNeeded(userId, payload.git);
|
||||
const resolvedAuth = await saveGitCredentialsIfNeeded(userId, payload.git);
|
||||
|
||||
return createStackFromGit({
|
||||
deploymentType: payload.deploymentType,
|
||||
@@ -132,13 +132,13 @@ async function createStackAndGitCredential(
|
||||
retryDeploy: payload.retryDeploy,
|
||||
staggerConfig: payload.staggerConfig,
|
||||
useManifestNamespaces: payload.useManifestNamespaces,
|
||||
repositoryUrl: newGitModel.RepositoryURL,
|
||||
repositoryReferenceName: newGitModel.RepositoryReferenceName,
|
||||
filePathInRepository: newGitModel.ComposeFilePathInRepository,
|
||||
repositoryAuthentication: newGitModel.RepositoryAuthentication,
|
||||
repositoryUsername: newGitModel.RepositoryUsername,
|
||||
repositoryPassword: newGitModel.RepositoryPassword,
|
||||
repositoryGitCredentialId: newGitModel.RepositoryGitCredentialID,
|
||||
repositoryUrl: payload.git.RepositoryURL,
|
||||
repositoryReferenceName: payload.git.RepositoryReferenceName,
|
||||
filePathInRepository: payload.git.ComposeFilePathInRepository,
|
||||
repositoryAuthentication: resolvedAuth.RepositoryAuthentication,
|
||||
repositoryUsername: resolvedAuth.RepositoryUsername,
|
||||
repositoryPassword: resolvedAuth.RepositoryPassword,
|
||||
repositoryGitCredentialId: resolvedAuth.RepositoryGitCredentialID,
|
||||
filesystemPath: payload.relativePathSettings?.FilesystemPath,
|
||||
supportRelativePath: payload.relativePathSettings?.SupportRelativePath,
|
||||
perDeviceConfigsGroupMatchType:
|
||||
@@ -146,7 +146,7 @@ async function createStackAndGitCredential(
|
||||
perDeviceConfigsMatchType:
|
||||
payload.relativePathSettings?.PerDeviceConfigsMatchType,
|
||||
perDeviceConfigsPath: payload.relativePathSettings?.PerDeviceConfigsPath,
|
||||
tlsSkipVerify: newGitModel.TLSSkipVerify,
|
||||
tlsSkipVerify: payload.git.TLSSkipVerify,
|
||||
autoUpdate: payload.autoUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
+9
-5
@@ -2,7 +2,10 @@ import { useQueryClient, useMutation } from '@tanstack/react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
||||
import { GitAuthModel } from '@/react/portainer/gitops/types';
|
||||
import {
|
||||
GitAuthModel,
|
||||
GitCredentialsModel,
|
||||
} from '@/react/portainer/gitops/types';
|
||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||
import { UserId } from '@/portainer/users/types';
|
||||
|
||||
@@ -82,10 +85,10 @@ export function useSaveCredentialsIfRequired() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveGitCredentialsIfNeeded<TGit extends GitAuthModel>(
|
||||
export async function saveGitCredentialsIfNeeded(
|
||||
userId: UserId,
|
||||
gitModel: TGit
|
||||
) {
|
||||
gitModel: GitAuthModel
|
||||
): Promise<GitCredentialsModel> {
|
||||
let credentialsId = gitModel.RepositoryGitCredentialID;
|
||||
let username = gitModel.RepositoryUsername;
|
||||
let password = gitModel.RepositoryPassword;
|
||||
@@ -112,9 +115,10 @@ export async function saveGitCredentialsIfNeeded<TGit extends GitAuthModel>(
|
||||
}
|
||||
|
||||
return {
|
||||
...gitModel,
|
||||
RepositoryAuthentication: gitModel.RepositoryAuthentication,
|
||||
RepositoryGitCredentialID: credentialsId,
|
||||
RepositoryUsername: username,
|
||||
RepositoryPassword: password,
|
||||
RepositoryAuthorizationType: gitModel.RepositoryAuthorizationType,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,8 +94,10 @@ async function uploadTLSFilesForEndpoint(
|
||||
if (!cert) {
|
||||
return null;
|
||||
}
|
||||
const formData = new FormData();
|
||||
formData.append('file', cert);
|
||||
try {
|
||||
return axios.post<void>(`upload/tls/${type}`, cert, {
|
||||
return axios.post<void>(`upload/tls/${type}`, formData, {
|
||||
params: { folder: id },
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { GitStackPayload } from '@/react/common/stacks/types';
|
||||
import { buildStackUrl } from '@/react/common/stacks/queries/buildUrl';
|
||||
import { queryKeys } from '@/react/common/stacks/queries/query-keys';
|
||||
import { withGlobalError } from '@/react-tools/react-query';
|
||||
|
||||
async function updateGitStackSettings(
|
||||
stackId: number,
|
||||
@@ -39,5 +40,6 @@ export function useUpdateGitStackSettings() {
|
||||
exact: true,
|
||||
});
|
||||
},
|
||||
...withGlobalError('Unable to save stack settings'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,11 +85,12 @@ async function createTemplateAndGitCredential(
|
||||
userId: UserId,
|
||||
{ Git: gitModel, ...values }: CreateTemplatePayload
|
||||
) {
|
||||
const newGitModel = await saveGitCredentialsIfNeeded(userId, gitModel);
|
||||
const resolvedAuth = await saveGitCredentialsIfNeeded(userId, gitModel);
|
||||
|
||||
return createTemplateFromGit({
|
||||
...values,
|
||||
...newGitModel,
|
||||
...gitModel,
|
||||
...resolvedAuth,
|
||||
...(values.EdgeSettings
|
||||
? {
|
||||
EdgeSettings: {
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"docker": "v29.3.0"
|
||||
"docker": "v29.4.1"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM alpine:latest AS production
|
||||
FROM alpine:3.23.4 AS production
|
||||
|
||||
LABEL org.opencontainers.image.title="Portainer" \
|
||||
org.opencontainers.image.description="Docker container management made simple, with the world’s most popular GUI-based container management platform." \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/portainer/portainer
|
||||
|
||||
go 1.25.8
|
||||
go 1.25.9
|
||||
|
||||
replace github.com/robfig/cron/v3 => github.com/robfig/cron/v3 v3.0.1 // Not actively maintained. Pinned to last known good version. Review needed when upgrading.
|
||||
|
||||
@@ -17,15 +17,18 @@ require (
|
||||
github.com/cbroglie/mustache v1.4.0
|
||||
github.com/compose-spec/compose-go/v2 v2.9.1
|
||||
github.com/containerd/containerd v1.7.30
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/coreos/go-semver v0.3.1
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v28.5.1+incompatible
|
||||
github.com/docker/compose/v2 v2.40.3
|
||||
github.com/docker/docker v28.5.1+incompatible
|
||||
github.com/fvbommel/sortorder v1.1.0
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814
|
||||
github.com/go-git/go-git/v5 v5.17.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.1
|
||||
github.com/go-git/go-billy/v5 v5.8.0
|
||||
github.com/go-git/go-git/v5 v5.18.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.11-0.20250407185144-d1298fcf754b
|
||||
github.com/gofrs/uuid v4.2.0+incompatible
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
@@ -54,20 +57,20 @@ require (
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
go.podman.io/image/v5 v5.37.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
|
||||
golang.org/x/mod v0.31.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/mod v0.33.0
|
||||
golang.org/x/oauth2 v0.35.0
|
||||
golang.org/x/sync v0.20.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
helm.sh/helm/v3 v3.20.0
|
||||
k8s.io/api v0.35.0
|
||||
k8s.io/apimachinery v0.35.0
|
||||
k8s.io/cli-runtime v0.35.0
|
||||
k8s.io/client-go v0.35.0
|
||||
k8s.io/kubectl v0.35.0
|
||||
helm.sh/helm/v3 v3.20.2
|
||||
k8s.io/api v0.35.1
|
||||
k8s.io/apimachinery v0.35.1
|
||||
k8s.io/cli-runtime v0.35.1
|
||||
k8s.io/client-go v0.35.1
|
||||
k8s.io/kubectl v0.35.1
|
||||
k8s.io/kubelet v0.35.0
|
||||
k8s.io/metrics v0.35.0
|
||||
k8s.io/metrics v0.35.1
|
||||
oras.land/oras-go/v2 v2.6.0
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
|
||||
)
|
||||
@@ -78,7 +81,7 @@ require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/DefangLabs/secret-detector v0.0.0-20250403165618-22662109213e // indirect
|
||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||
@@ -109,6 +112,7 @@ require (
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/buger/goterm v1.0.4 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/cloudflare/cfssl v1.6.4 // indirect
|
||||
@@ -117,7 +121,6 @@ require (
|
||||
github.com/containerd/containerd/api v1.9.0 // indirect
|
||||
github.com/containerd/containerd/v2 v2.1.5 // indirect
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.1 // indirect
|
||||
@@ -127,7 +130,6 @@ require (
|
||||
github.com/containers/ocicrypt v1.2.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/buildx v0.29.1 // indirect
|
||||
github.com/docker/cli-docs-tool v0.10.0 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
@@ -147,10 +149,9 @@ require (
|
||||
github.com/fsnotify/fsevents v0.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -170,7 +171,7 @@ require (
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
@@ -211,7 +212,7 @@ require (
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/spdystream v0.5.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/capability v0.4.0 // indirect
|
||||
github.com/moby/sys/mountinfo v0.7.2 // indirect
|
||||
@@ -274,38 +275,38 @@ require (
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.podman.io/storage v1.60.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.35.0 // indirect
|
||||
k8s.io/apiserver v0.35.0 // indirect
|
||||
k8s.io/component-base v0.35.0 // indirect
|
||||
k8s.io/component-helpers v0.35.0 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.35.1 // indirect
|
||||
k8s.io/apiserver v0.35.1 // indirect
|
||||
k8s.io/component-base v0.35.1 // indirect
|
||||
k8s.io/component-helpers v0.35.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
|
||||
@@ -11,8 +11,8 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkk
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
@@ -57,6 +57,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
|
||||
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
|
||||
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
|
||||
@@ -126,6 +128,8 @@ github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprY
|
||||
github.com/cbroglie/mustache v1.4.0/go.mod h1:SS1FTIghy0sjse4DUVGV1k/40B1qE1XkD9DtDsHo9iM=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -272,8 +276,8 @@ github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814 h1:gWvniJ4GbFfkf70
|
||||
github.com/g07cha/defender v0.0.0-20180505193036-5665c627c814/go.mod h1:secRm32Ro77eD23BmPVbgLbWN+JWDw7pJszenjxI4bI=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
@@ -282,13 +286,13 @@ github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDz
|
||||
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
|
||||
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
|
||||
github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
|
||||
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-ldap/ldap/v3 v3.4.1 h1:fU/0xli6HY02ocbMuozHAYsaHLcnkLjvho2r5a34BUU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.1/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11-0.20250407185144-d1298fcf754b h1:Zb8nf4EaxwB5RgQRF81vAR0/XIxebggomQQhFprBwLo=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11-0.20250407185144-d1298fcf754b/go.mod h1:2pKywWBl+KdQvQnNOruU8zGPkECFn5NfhuuuHxJObFo=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -379,8 +383,8 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=
|
||||
github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -390,6 +394,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
@@ -412,6 +418,18 @@ github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaB
|
||||
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE=
|
||||
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
|
||||
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc=
|
||||
@@ -531,8 +549,8 @@ github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y=
|
||||
github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk=
|
||||
@@ -784,22 +802,22 @@ go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0/go.mod h1:CosX/aS4eHnG9D7nESYpV753l4j9q5j3SL/PUYd2lR8=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0 h1:0NIXxOCFx+SKbhCVxwl3ETG8ClLPAa0KuKV6p3yhxP8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.35.0/go.mod h1:ChZSJbbfbl/DcRZNc9Gqh6DYGlfjw4PvO1pEOZH1ZsE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU=
|
||||
@@ -810,18 +828,18 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsu
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
|
||||
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
|
||||
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
|
||||
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
|
||||
go.podman.io/image/v5 v5.37.0 h1:yzgQybwuWIIeK63hu+mQqna/wOh96XD5cpVc6j8Dg5M=
|
||||
go.podman.io/image/v5 v5.37.0/go.mod h1:+s2Sx5dia/jVeT8tI3r2NAPrARMiDdbEq3QPIQogx3I=
|
||||
go.podman.io/storage v1.60.0 h1:bWNSrR58nxg39VNFDSx3m0AswbvyzPGOo5XsUfomTao=
|
||||
@@ -839,7 +857,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
@@ -852,8 +869,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -863,8 +880,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -885,11 +902,11 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -903,8 +920,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -939,8 +956,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -952,8 +969,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
@@ -968,8 +985,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -980,26 +997,28 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.0.5/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII=
|
||||
@@ -1035,34 +1054,34 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
helm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50=
|
||||
helm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o=
|
||||
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
|
||||
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
|
||||
k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4=
|
||||
k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU=
|
||||
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
|
||||
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4=
|
||||
k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds=
|
||||
k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE=
|
||||
k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY=
|
||||
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
|
||||
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
|
||||
k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94=
|
||||
k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0=
|
||||
k8s.io/component-helpers v0.35.0 h1:wcXv7HJRksgVjM4VlXJ1CNFBpyDHruRI99RrBtrJceA=
|
||||
k8s.io/component-helpers v0.35.0/go.mod h1:ahX0m/LTYmu7fL3W8zYiIwnQ/5gT28Ex4o2pymF63Co=
|
||||
helm.sh/helm/v3 v3.20.2 h1:binM4rvPx5DcNsa1sIt7UZi55lRbu3pZUFmQkSoRh48=
|
||||
helm.sh/helm/v3 v3.20.2/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww=
|
||||
k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
|
||||
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
|
||||
k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w=
|
||||
k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ=
|
||||
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
|
||||
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs=
|
||||
k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4=
|
||||
k8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE=
|
||||
k8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw=
|
||||
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
|
||||
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
|
||||
k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE=
|
||||
k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs=
|
||||
k8s.io/component-helpers v0.35.1 h1:vwQ/cAfnVwaPeSXTu4DdK3d3n11Lugc5vMb6EV809ZY=
|
||||
k8s.io/component-helpers v0.35.1/go.mod h1:HQqMwUk68Yyxgj92dJ+J1w/qbx9M0QR0eZ680m/o+Rk=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc=
|
||||
k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo=
|
||||
k8s.io/kubectl v0.35.1 h1:zP3Er8C5i1dcAFUMh9Eva0kVvZHptXIn/+8NtRWMxwg=
|
||||
k8s.io/kubectl v0.35.1/go.mod h1:cQ2uAPs5IO/kx8R5s5J3Ihv3VCYwrx0obCXum0CvnXo=
|
||||
k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c=
|
||||
k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA=
|
||||
k8s.io/metrics v0.35.0 h1:xVFoqtAGm2dMNJAcB5TFZJPCen0uEqqNt52wW7ABbX8=
|
||||
k8s.io/metrics v0.35.0/go.mod h1:g2Up4dcBygZi2kQSEQVDByFs+VUwepJMzzQLJJLpq4M=
|
||||
k8s.io/metrics v0.35.1 h1:MUcrUcWlq81XiripkydzCGsY9zQawDXfP9IICNNcVVw=
|
||||
k8s.io/metrics v0.35.1/go.mod h1:9x7xWOAOiWzHA0vaqLgSE4PXF3vyT5ts5XIbx8OSjiI=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
|
||||
|
||||
+3
-4
@@ -2,7 +2,7 @@
|
||||
"author": "Portainer.io",
|
||||
"name": "@portainer/ce",
|
||||
"homepage": "http://portainer.io",
|
||||
"version": "2.39.1",
|
||||
"version": "2.39.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:portainer/portainer.git"
|
||||
@@ -100,7 +100,7 @@
|
||||
"formik": "^2.2.9",
|
||||
"i18next": "^21.3.3",
|
||||
"i18next-browser-languagedetector": "^6.1.2",
|
||||
"i18next-http-backend": "^1.3.1",
|
||||
"i18next-http-backend": "^3.0.4",
|
||||
"jquery": "^3.6.0",
|
||||
"js-base64": "^3.7.2",
|
||||
"js-yaml": "^3.14.2",
|
||||
@@ -229,8 +229,7 @@
|
||||
"style-loader": "^3.3.3",
|
||||
"swagger2openapi": "^7.0.8",
|
||||
"tailwindcss": "3.3.3",
|
||||
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||
"typescript": "^5.5.2",
|
||||
"typescript": "^6.0.2",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^4.3.1",
|
||||
"vitest": "^4",
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestValidateHelmRepositoryURL(t *testing.T) {
|
||||
func Test_ValidateSeedsCacheAndSearchUsesCache(t *testing.T) {
|
||||
const indexYAML = "apiVersion: v1\nentries: {}\ngenerated: \"2020-01-01T00:00:00Z\"\n"
|
||||
|
||||
var requestCount int32
|
||||
var requestCount atomic.Int32
|
||||
var fail bool
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -107,7 +107,7 @@ func Test_ValidateSeedsCacheAndSearchUsesCache(t *testing.T) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
atomic.AddInt32(&requestCount, 1)
|
||||
requestCount.Add(1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(indexYAML))
|
||||
return
|
||||
@@ -126,5 +126,5 @@ func Test_ValidateSeedsCacheAndSearchUsesCache(t *testing.T) {
|
||||
// validate cache is used
|
||||
err := ValidateHelmRepositoryURL(srv.URL, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int32(1), atomic.LoadInt32(&requestCount))
|
||||
require.Equal(t, int32(1), requestCount.Load())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -54,8 +55,20 @@ func (c *Client) DeleteDynamic(ctx context.Context, manifests []string) (string,
|
||||
continue
|
||||
}
|
||||
|
||||
var content string
|
||||
if isManifestFile(manifest) {
|
||||
data, err := os.ReadFile(manifest)
|
||||
if err != nil {
|
||||
errs = errors.Join(errs, fmt.Errorf("failed to read file %s: %w", manifest, err))
|
||||
continue
|
||||
}
|
||||
content = string(data)
|
||||
} else {
|
||||
content = manifest
|
||||
}
|
||||
|
||||
// Split by document separator if multiple resources in one manifest
|
||||
for resource := range strings.SplitSeq(manifest, "\n---\n") {
|
||||
for resource := range strings.SplitSeq(content, "\n---\n") {
|
||||
resource = strings.TrimSpace(resource)
|
||||
if resource == "" {
|
||||
continue
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package compose
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -12,20 +13,28 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/logs"
|
||||
"github.com/portainer/portainer/pkg/libstack"
|
||||
retry "github.com/portainer/portainer/pkg/retry"
|
||||
|
||||
"github.com/compose-spec/compose-go/v2/cli"
|
||||
"github.com/compose-spec/compose-go/v2/types"
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/docker/cli/cli/command"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/flags"
|
||||
cmdcompose "github.com/docker/compose/v2/cmd/compose"
|
||||
"github.com/docker/compose/v2/pkg/api"
|
||||
"github.com/docker/compose/v2/pkg/compose"
|
||||
"github.com/docker/compose/v2/pkg/utils"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/segmentio/encoding/json"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -230,10 +239,139 @@ func (c *ComposeDeployer) Remove(ctx context.Context, projectName string, filePa
|
||||
return nil
|
||||
}
|
||||
|
||||
// Separator is used for naming components
|
||||
const separator = "-"
|
||||
|
||||
// getImageNameOrDefault computes the default image name for a service
|
||||
func getImageNameOrDefault(service types.ServiceConfig, projectName string) string {
|
||||
imageName := service.Image
|
||||
if imageName == "" {
|
||||
imageName = projectName + separator + service.Name
|
||||
}
|
||||
return imageName
|
||||
}
|
||||
|
||||
// encodeRegistryAuth finds the registry credentials for the given image and returns
|
||||
// the base64-encoded auth string expected by the Docker service API.
|
||||
// Returns an empty string (no error) when no matching credentials are found.
|
||||
func encodeRegistryAuth(image string, registries []configtypes.AuthConfig) (string, error) {
|
||||
named, err := reference.ParseNormalizedNamed(image)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse image reference %q: %w", image, err)
|
||||
}
|
||||
|
||||
domain := reference.Domain(named)
|
||||
if domain == "docker.io" {
|
||||
domain = registry.IndexServer
|
||||
}
|
||||
|
||||
for _, r := range registries {
|
||||
if r.ServerAddress == domain {
|
||||
encoded, err := registrytypes.EncodeAuthConfig(registrytypes.AuthConfig{
|
||||
Username: r.Username,
|
||||
Password: r.Password,
|
||||
ServerAddress: r.ServerAddress,
|
||||
Auth: r.Auth,
|
||||
IdentityToken: r.IdentityToken,
|
||||
RegistryToken: r.RegistryToken,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode auth for registry %s: %w", domain, err)
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// Pull pulls images
|
||||
func (c *ComposeDeployer) Pull(ctx context.Context, filePaths []string, options libstack.Options) error {
|
||||
if err := c.withComposeService(ctx, filePaths, options, func(composeService api.Compose, project *types.Project) error {
|
||||
return composeService.Pull(ctx, project, api.PullOptions{})
|
||||
if err := withCli(ctx, options, func(ctx context.Context, cli *command.DockerCli) error {
|
||||
project, err := createProject(ctx, filePaths, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create compose project: %w", err)
|
||||
}
|
||||
|
||||
for _, s := range project.Services {
|
||||
imageName := getImageNameOrDefault(s, project.Name)
|
||||
encodedAuth, err := encodeRegistryAuth(imageName, options.Registries)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode registry auth: %w", err)
|
||||
}
|
||||
|
||||
_, err = retry.RetryWithWarnings("Pull image: "+imageName, retry.Default, func() (string, error) {
|
||||
_, err := cli.Client().ImageInspect(ctx, imageName)
|
||||
if cerrdefs.IsNotFound(err) {
|
||||
reader, err := cli.Client().ImagePull(ctx, imageName, image.PullOptions{
|
||||
Platform: s.Platform,
|
||||
RegistryAuth: encodedAuth,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to pull image: %w", err)
|
||||
}
|
||||
|
||||
defer logs.CloseAndLogErr(reader)
|
||||
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
message := scanner.Text()
|
||||
log.Debug().
|
||||
Str("ProjectName", options.ProjectName).
|
||||
Str("Host", options.Host).
|
||||
Str("Image", imageName).
|
||||
Msg(message)
|
||||
|
||||
var m jsonmessage.JSONMessage
|
||||
err := json.Unmarshal([]byte(message), &m)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("ProjectName", options.ProjectName).
|
||||
Str("Host", options.Host).
|
||||
Str("Image", imageName).
|
||||
Msg("ComposeDeployer.Pull: failed to json Unmarshal image pull message.")
|
||||
return "", fmt.Errorf("failed to json Unmarshal image pull message: %w", err)
|
||||
}
|
||||
|
||||
if m.Error != nil {
|
||||
log.Error().
|
||||
Err(m.Error).
|
||||
Str("ProjectName", options.ProjectName).
|
||||
Str("Host", options.Host).
|
||||
Str("Image", imageName).
|
||||
Msg("ComposeDeployer.Pull: error pulling image")
|
||||
return "", fmt.Errorf("error pulling image: %w", m.Error)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("ProjectName", options.ProjectName).
|
||||
Str("Host", options.Host).
|
||||
Str("Image", imageName).
|
||||
Msg("ComposeDeployer.Pull: error reading from pull reader")
|
||||
return "", fmt.Errorf("error reading from pull reader: %w", err)
|
||||
}
|
||||
|
||||
return "", nil
|
||||
} else if err != nil {
|
||||
return "", fmt.Errorf("failed to inspect image: %w", err)
|
||||
} else {
|
||||
return "", nil
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("ProjectName", options.ProjectName).
|
||||
Str("Host", options.Host).
|
||||
Str("Image", imageName).
|
||||
Msg("ComposeDeployer.Pull: failed to pull image")
|
||||
return fmt.Errorf("failed to pull image: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("compose pull operation failed: %w", err)
|
||||
}
|
||||
@@ -355,10 +493,15 @@ func createProject(ctx context.Context, configFilepaths []string, options libsta
|
||||
}
|
||||
|
||||
var osPortainerEnvVars []string
|
||||
var composeEnvVars []string
|
||||
for _, ev := range os.Environ() {
|
||||
if strings.HasPrefix(ev, portainerEnvVarsPrefix) {
|
||||
osPortainerEnvVars = append(osPortainerEnvVars, ev)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ev, "COMPOSE_") {
|
||||
composeEnvVars = append(composeEnvVars, ev)
|
||||
}
|
||||
}
|
||||
|
||||
projectOptions, err := cli.NewProjectOptions(configFilepaths,
|
||||
@@ -367,6 +510,7 @@ func createProject(ctx context.Context, configFilepaths []string, options libsta
|
||||
cli.WithoutEnvironmentResolution,
|
||||
cli.WithResolvedPaths(!slices.Contains(options.ConfigOptions, "--no-path-resolution")),
|
||||
cli.WithEnv(osPortainerEnvVars),
|
||||
cli.WithEnv(composeEnvVars),
|
||||
cli.WithEnv(options.Env),
|
||||
cli.WithEnvFiles(envFiles...),
|
||||
func(o *cli.ProjectOptions) error {
|
||||
|
||||
@@ -139,7 +139,6 @@ configs:
|
||||
require.NoError(t, err)
|
||||
|
||||
require.False(t, containerExists(containerName))
|
||||
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
@@ -1144,6 +1143,23 @@ func Test_createProject(t *testing.T) {
|
||||
},
|
||||
expectedProject: expectedSimpleComposeProject("", map[string]string{"PORTAINER_WEB_FOLDER": "html-1"}),
|
||||
},
|
||||
{
|
||||
name: "OS COMPOSE_ vars are passed to project",
|
||||
filesToCreate: map[string]string{
|
||||
"docker-compose.yml": testSimpleComposeConfig,
|
||||
},
|
||||
configFilepaths: []string{dir + "/docker-compose.yml"},
|
||||
options: libstack.Options{
|
||||
ProjectName: projectName,
|
||||
},
|
||||
osEnv: map[string]string{
|
||||
"COMPOSE_PARALLEL_LIMIT": "4",
|
||||
"other_var": "something",
|
||||
},
|
||||
expectedProject: expectedSimpleComposeProject("", map[string]string{
|
||||
"COMPOSE_PARALLEL_LIMIT": "4",
|
||||
}),
|
||||
},
|
||||
{
|
||||
name: "Env Vars in compose file, compose env file, env, os, and env_file",
|
||||
filesToCreate: map[string]string{
|
||||
@@ -1450,6 +1466,100 @@ func Test_CredentialsStore_Behavior(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetImageNameOrDefault(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
service types.ServiceConfig
|
||||
projectName string
|
||||
expectedName string
|
||||
}{
|
||||
{
|
||||
name: "service with explicit image",
|
||||
service: types.ServiceConfig{Name: "web", Image: "nginx:latest"},
|
||||
projectName: "myproject",
|
||||
expectedName: "nginx:latest",
|
||||
},
|
||||
{
|
||||
name: "service without image uses default",
|
||||
service: types.ServiceConfig{Name: "web", Image: ""},
|
||||
projectName: "myproject",
|
||||
expectedName: "myproject-web",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotName := getImageNameOrDefault(tc.service, tc.projectName)
|
||||
require.Equal(t, tc.expectedName, gotName)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeRegistryAuth(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
image string
|
||||
registries []configtypes.AuthConfig
|
||||
expectRegistryAuth string
|
||||
expectError string
|
||||
}{
|
||||
{
|
||||
name: "matching registry returns encoded auth",
|
||||
image: "myregistry.example.com/myimage:latest",
|
||||
registries: []configtypes.AuthConfig{
|
||||
{ServerAddress: "myregistry.example.com", Username: "user", Password: "pass"},
|
||||
},
|
||||
expectRegistryAuth: "eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6InBhc3MiLCJzZXJ2ZXJhZGRyZXNzIjoibXlyZWdpc3RyeS5leGFtcGxlLmNvbSJ9",
|
||||
},
|
||||
{
|
||||
name: "unknown registry returns empty string",
|
||||
image: "myregistry.example.com/myimage:latest",
|
||||
registries: []configtypes.AuthConfig{
|
||||
{ServerAddress: "other.registry.com", Username: "user", Password: "pass"},
|
||||
},
|
||||
expectRegistryAuth: "",
|
||||
},
|
||||
{
|
||||
name: "docker.io image matches index server",
|
||||
image: "alpine:latest",
|
||||
registries: []configtypes.AuthConfig{
|
||||
{ServerAddress: "https://index.docker.io/v1/", Username: "user", Password: "pass"},
|
||||
},
|
||||
expectRegistryAuth: "eyJ1c2VybmFtZSI6InVzZXIiLCJwYXNzd29yZCI6InBhc3MiLCJzZXJ2ZXJhZGRyZXNzIjoiaHR0cHM6Ly9pbmRleC5kb2NrZXIuaW8vdjEvIn0=",
|
||||
},
|
||||
{
|
||||
name: "invalid image reference returns error",
|
||||
image: "INVALID::IMAGE",
|
||||
registries: []configtypes.AuthConfig{},
|
||||
expectRegistryAuth: "",
|
||||
expectError: `failed to parse image reference "INVALID::IMAGE": invalid reference format: repository name (library/INVALID) must be lowercase`,
|
||||
},
|
||||
{
|
||||
name: "multiple registries only matching one used",
|
||||
image: "registry-b.example.com/image:latest",
|
||||
registries: []configtypes.AuthConfig{
|
||||
{ServerAddress: "registry-a.example.com", Username: "user-a", Password: "pass-a"},
|
||||
{ServerAddress: "registry-b.example.com", Username: "user-b", Password: "pass-b"},
|
||||
},
|
||||
expectRegistryAuth: "eyJ1c2VybmFtZSI6InVzZXItYiIsInBhc3N3b3JkIjoicGFzcy1iIiwic2VydmVyYWRkcmVzcyI6InJlZ2lzdHJ5LWIuZXhhbXBsZS5jb20ifQ==",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gotRegistryAuth, err := encodeRegistryAuth(tc.image, tc.registries)
|
||||
|
||||
if tc.expectError == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tc.expectError)
|
||||
}
|
||||
|
||||
require.Equal(t, tc.expectRegistryAuth, gotRegistryAuth)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func createMockComposeService(command.Cli, ...compose.Option) api.Compose {
|
||||
return &mockComposeService{}
|
||||
}
|
||||
|
||||
Generated
+139
-165
@@ -226,8 +226,8 @@ importers:
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
i18next-http-backend:
|
||||
specifier: ^1.3.1
|
||||
version: 1.4.1
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
jquery:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0
|
||||
@@ -381,10 +381,10 @@ importers:
|
||||
version: 3.0.0(webpack@5.105.0)
|
||||
'@storybook/react':
|
||||
specifier: 8.6.15
|
||||
version: 8.6.15(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)
|
||||
version: 8.6.15(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)
|
||||
'@storybook/react-webpack5':
|
||||
specifier: 8.6.15
|
||||
version: 8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)(webpack-cli@6.0.1)
|
||||
version: 8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)(webpack-cli@6.0.1)
|
||||
'@svgr/webpack':
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0
|
||||
@@ -459,16 +459,16 @@ importers:
|
||||
version: 3.4.13
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^6.7.4
|
||||
version: 6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint@8.50.0)(typescript@5.5.2)
|
||||
version: 6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint@8.50.0)(typescript@6.0.2)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^6.7.4
|
||||
version: 6.7.4(eslint@8.50.0)(typescript@5.5.2)
|
||||
version: 6.7.4(eslint@8.50.0)(typescript@6.0.2)
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^4
|
||||
version: 4.0.18(vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(terser@5.44.1)(yaml@1.10.2))
|
||||
version: 4.0.18(vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(terser@5.44.1)(yaml@1.10.2))
|
||||
'@vitest/eslint-plugin':
|
||||
specifier: ^1.6.7
|
||||
version: 1.6.7(eslint@8.50.0)(typescript@5.5.2)(vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(terser@5.44.1)(yaml@1.10.2))
|
||||
version: 1.6.7(eslint@8.50.0)(typescript@6.0.2)(vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(terser@5.44.1)(yaml@1.10.2))
|
||||
auto-ngtemplate-loader:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2(webpack@5.105.0)
|
||||
@@ -507,7 +507,7 @@ importers:
|
||||
version: 19.0.4(eslint-plugin-import@2.28.1)(eslint-plugin-jsx-a11y@6.7.1(eslint@8.50.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.50.0))(eslint-plugin-react@7.33.2(eslint@8.50.0))(eslint@8.50.0)
|
||||
eslint-config-airbnb-typescript:
|
||||
specifier: ^17.1.0
|
||||
version: 17.1.0(@typescript-eslint/eslint-plugin@6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint@8.50.0)(typescript@5.5.2))(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0)
|
||||
version: 17.1.0(@typescript-eslint/eslint-plugin@6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint@8.50.0)(typescript@6.0.2))(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0)
|
||||
eslint-config-prettier:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.0(eslint@8.50.0)
|
||||
@@ -516,13 +516,13 @@ importers:
|
||||
version: 1.1.2(eslint-plugin-import@2.28.1)
|
||||
eslint-import-resolver-typescript:
|
||||
specifier: ^3.6.1
|
||||
version: 3.6.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0)
|
||||
version: 3.6.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0)
|
||||
eslint-plugin-eslint-comments:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(eslint@8.50.0)
|
||||
eslint-plugin-import:
|
||||
specifier: ^2.28.1
|
||||
version: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
version: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-plugin-jsx-a11y:
|
||||
specifier: ^6.7.1
|
||||
version: 6.7.1(eslint@8.50.0)
|
||||
@@ -540,7 +540,7 @@ importers:
|
||||
version: 1.10.0(eslint@8.50.0)
|
||||
eslint-plugin-storybook:
|
||||
specifier: ^9
|
||||
version: 9.1.17(eslint@8.50.0)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)
|
||||
version: 9.1.17(eslint@8.50.0)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)
|
||||
html-loader:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(webpack@5.105.0)
|
||||
@@ -564,10 +564,10 @@ importers:
|
||||
version: 2.7.6(webpack@5.105.0)
|
||||
msw:
|
||||
specifier: ^2.12.10
|
||||
version: 2.12.10(@types/node@25.0.3)(typescript@5.5.2)
|
||||
version: 2.12.10(@types/node@25.0.3)(typescript@6.0.2)
|
||||
msw-storybook-addon:
|
||||
specifier: ^2.0.6
|
||||
version: 2.0.6(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))
|
||||
version: 2.0.6(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))
|
||||
ngtemplate-loader:
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
@@ -585,7 +585,7 @@ importers:
|
||||
version: 0.5.4(prettier@3.0.3)
|
||||
react-docgen-typescript-plugin:
|
||||
specifier: ^1.0.5
|
||||
version: 1.0.5(typescript@5.5.2)(webpack@5.105.0)
|
||||
version: 1.0.5(typescript@6.0.2)(webpack@5.105.0)
|
||||
source-map-loader:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(webpack@5.105.0)
|
||||
@@ -607,21 +607,18 @@ importers:
|
||||
tailwindcss:
|
||||
specifier: 3.3.3
|
||||
version: 3.3.3
|
||||
tsconfig-paths-webpack-plugin:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
typescript:
|
||||
specifier: ^5.5.2
|
||||
version: 5.5.2
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
vite-plugin-svgr:
|
||||
specifier: ^4.5.0
|
||||
version: 4.5.0(rollup@4.54.0)(vite@7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1(typescript@5.5.2)(vite@7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))
|
||||
version: 4.3.1(typescript@6.0.2)(vite@7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))
|
||||
vitest:
|
||||
specifier: ^4
|
||||
version: 4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(terser@5.44.1)(yaml@1.10.2)
|
||||
version: 4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(terser@5.44.1)(yaml@1.10.2)
|
||||
webpack:
|
||||
specifier: ^5.105.0
|
||||
version: 5.105.0(@swc/core@1.15.11)(esbuild@0.21.5)(webpack-cli@6.0.1)
|
||||
@@ -4295,8 +4292,8 @@ packages:
|
||||
crelt@1.0.6:
|
||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||
|
||||
cross-fetch@3.1.5:
|
||||
resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==}
|
||||
cross-fetch@4.1.0:
|
||||
resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
@@ -5413,8 +5410,8 @@ packages:
|
||||
i18next-browser-languagedetector@6.1.2:
|
||||
resolution: {integrity: sha512-YDzIGHhMRvr7M+c8B3EQUKyiMBhfqox4o1qkFvt4QXuu5V2cxf74+NCr+VEkUuU0y+RwcupA238eeolW1Yn80g==}
|
||||
|
||||
i18next-http-backend@1.4.1:
|
||||
resolution: {integrity: sha512-s4Q9hK2jS29iyhniMP82z+yYY8riGTrWbnyvsSzi5TaF7Le4E7b5deTmtuaRuab9fdDcYXtcwdBgawZG+JCEjA==}
|
||||
i18next-http-backend@3.0.4:
|
||||
resolution: {integrity: sha512-udwrBIE6cNpqn1gRAqRULq3+7MzIIuaiKRWrz++dVz5SqWW2VwXmPJtAgkI0JtMLFaADC9qNmnZAxWAhsxXx2g==}
|
||||
|
||||
i18next@20.6.1:
|
||||
resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
|
||||
@@ -6219,15 +6216,6 @@ packages:
|
||||
resolution: {integrity: sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
|
||||
node-fetch@2.6.7:
|
||||
resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
@@ -7921,10 +7909,6 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
tsconfig-paths-webpack-plugin@4.1.0:
|
||||
resolution: {integrity: sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
tsconfig-paths@3.14.2:
|
||||
resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==}
|
||||
|
||||
@@ -7983,8 +7967,8 @@ packages:
|
||||
typed-array-length@1.0.4:
|
||||
resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==}
|
||||
|
||||
typescript@5.5.2:
|
||||
resolution: {integrity: sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==}
|
||||
typescript@6.0.2:
|
||||
resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
@@ -10748,7 +10732,7 @@ snapshots:
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
|
||||
'@storybook/builder-webpack5@8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)(webpack-cli@6.0.1)':
|
||||
'@storybook/builder-webpack5@8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)(webpack-cli@6.0.1)':
|
||||
dependencies:
|
||||
'@storybook/core-webpack': 8.6.15(storybook@8.6.15(prettier@3.0.3))
|
||||
'@types/semver': 7.7.1
|
||||
@@ -10758,7 +10742,7 @@ snapshots:
|
||||
constants-browserify: 1.0.0
|
||||
css-loader: 6.8.1(webpack@5.105.0)
|
||||
es-module-lexer: 1.5.4
|
||||
fork-ts-checker-webpack-plugin: 8.0.0(typescript@5.5.2)(webpack@5.105.0)
|
||||
fork-ts-checker-webpack-plugin: 8.0.0(typescript@6.0.2)(webpack@5.105.0)
|
||||
html-webpack-plugin: 5.5.3(webpack@5.105.0)
|
||||
magic-string: 0.30.14
|
||||
path-browserify: 1.0.1
|
||||
@@ -10776,7 +10760,7 @@ snapshots:
|
||||
webpack-hot-middleware: 2.25.3
|
||||
webpack-virtual-modules: 0.6.1
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
@@ -10829,11 +10813,11 @@ snapshots:
|
||||
dependencies:
|
||||
storybook: 8.6.15(prettier@3.0.3)
|
||||
|
||||
'@storybook/preset-react-webpack@8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)(webpack-cli@6.0.1)':
|
||||
'@storybook/preset-react-webpack@8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)(webpack-cli@6.0.1)':
|
||||
dependencies:
|
||||
'@storybook/core-webpack': 8.6.15(storybook@8.6.15(prettier@3.0.3))
|
||||
'@storybook/react': 8.6.15(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)
|
||||
'@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@5.5.2)(webpack@5.105.0)
|
||||
'@storybook/react': 8.6.15(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)
|
||||
'@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@6.0.2)(webpack@5.105.0)
|
||||
'@types/semver': 7.7.1
|
||||
find-up: 5.0.0
|
||||
magic-string: 0.30.14
|
||||
@@ -10846,7 +10830,7 @@ snapshots:
|
||||
tsconfig-paths: 4.2.0
|
||||
webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.21.5)(webpack-cli@6.0.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- '@storybook/test'
|
||||
- '@swc/core'
|
||||
@@ -10859,16 +10843,16 @@ snapshots:
|
||||
dependencies:
|
||||
storybook: 8.6.15(prettier@3.0.3)
|
||||
|
||||
'@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@5.5.2)(webpack@5.105.0)':
|
||||
'@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@6.0.2)(webpack@5.105.0)':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
endent: 2.1.0
|
||||
find-cache-dir: 3.3.2
|
||||
flat-cache: 3.0.4
|
||||
micromatch: 4.0.8
|
||||
react-docgen-typescript: 2.2.2(typescript@5.5.2)
|
||||
react-docgen-typescript: 2.2.2(typescript@6.0.2)
|
||||
tslib: 2.8.1
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.21.5)(webpack-cli@6.0.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -10879,16 +10863,16 @@ snapshots:
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
storybook: 8.6.15(prettier@3.0.3)
|
||||
|
||||
'@storybook/react-webpack5@8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)(webpack-cli@6.0.1)':
|
||||
'@storybook/react-webpack5@8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)(webpack-cli@6.0.1)':
|
||||
dependencies:
|
||||
'@storybook/builder-webpack5': 8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)(webpack-cli@6.0.1)
|
||||
'@storybook/preset-react-webpack': 8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)(webpack-cli@6.0.1)
|
||||
'@storybook/react': 8.6.15(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)
|
||||
'@storybook/builder-webpack5': 8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)(webpack-cli@6.0.1)
|
||||
'@storybook/preset-react-webpack': 8.6.15(@swc/core@1.15.11)(esbuild@0.21.5)(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)(webpack-cli@6.0.1)
|
||||
'@storybook/react': 8.6.15(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)
|
||||
react: 17.0.2
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
storybook: 8.6.15(prettier@3.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- '@storybook/test'
|
||||
- '@swc/core'
|
||||
@@ -10897,7 +10881,7 @@ snapshots:
|
||||
- uglify-js
|
||||
- webpack-cli
|
||||
|
||||
'@storybook/react@8.6.15(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2)':
|
||||
'@storybook/react@8.6.15(react-dom@17.0.2(react@17.0.2))(react@17.0.2)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@storybook/components': 8.6.15(storybook@8.6.15(prettier@3.0.3))
|
||||
'@storybook/global': 5.0.0
|
||||
@@ -10909,7 +10893,7 @@ snapshots:
|
||||
react-dom: 17.0.2(react@17.0.2)
|
||||
storybook: 8.6.15(prettier@3.0.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
|
||||
'@storybook/theming@8.6.15(storybook@8.6.15(prettier@3.0.3))':
|
||||
dependencies:
|
||||
@@ -11385,13 +11369,13 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/yargs-parser': 21.0.0
|
||||
|
||||
'@typescript-eslint/eslint-plugin@6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint@8.50.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/eslint-plugin@6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint@8.50.0)(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.9.1
|
||||
'@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@6.0.2)
|
||||
'@typescript-eslint/scope-manager': 6.7.4
|
||||
'@typescript-eslint/type-utils': 6.7.4(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/type-utils': 6.7.4(eslint@8.50.0)(typescript@6.0.2)
|
||||
'@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@6.0.2)
|
||||
'@typescript-eslint/visitor-keys': 6.7.4
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
eslint: 8.50.0
|
||||
@@ -11399,40 +11383,40 @@ snapshots:
|
||||
ignore: 5.2.4
|
||||
natural-compare: 1.4.0
|
||||
semver: 7.5.4
|
||||
ts-api-utils: 1.0.1(typescript@5.5.2)
|
||||
ts-api-utils: 1.0.1(typescript@6.0.2)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 6.7.4
|
||||
'@typescript-eslint/types': 6.7.4
|
||||
'@typescript-eslint/typescript-estree': 6.7.4(typescript@5.5.2)
|
||||
'@typescript-eslint/typescript-estree': 6.7.4(typescript@6.0.2)
|
||||
'@typescript-eslint/visitor-keys': 6.7.4
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
eslint: 8.50.0
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.54.0(typescript@5.5.2)':
|
||||
'@typescript-eslint/project-service@8.54.0(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.5.2)
|
||||
'@typescript-eslint/tsconfig-utils': 8.54.0(typescript@6.0.2)
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
debug: 4.4.3
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/project-service@8.55.0(typescript@5.5.2)':
|
||||
'@typescript-eslint/project-service@8.55.0(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.5.2)
|
||||
'@typescript-eslint/tsconfig-utils': 8.55.0(typescript@6.0.2)
|
||||
'@typescript-eslint/types': 8.55.0
|
||||
debug: 4.4.3
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -11451,23 +11435,23 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.55.0
|
||||
'@typescript-eslint/visitor-keys': 8.55.0
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.5.2)':
|
||||
'@typescript-eslint/tsconfig-utils@8.54.0(typescript@6.0.2)':
|
||||
dependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
|
||||
'@typescript-eslint/tsconfig-utils@8.55.0(typescript@5.5.2)':
|
||||
'@typescript-eslint/tsconfig-utils@8.55.0(typescript@6.0.2)':
|
||||
dependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
|
||||
'@typescript-eslint/type-utils@6.7.4(eslint@8.50.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/type-utils@6.7.4(eslint@8.50.0)(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 6.7.4(typescript@5.5.2)
|
||||
'@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/typescript-estree': 6.7.4(typescript@6.0.2)
|
||||
'@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@6.0.2)
|
||||
debug: 4.4.3
|
||||
eslint: 8.50.0
|
||||
ts-api-utils: 1.0.1(typescript@5.5.2)
|
||||
ts-api-utils: 1.0.1(typescript@6.0.2)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -11477,7 +11461,7 @@ snapshots:
|
||||
|
||||
'@typescript-eslint/types@8.55.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@6.7.4(typescript@5.5.2)':
|
||||
'@typescript-eslint/typescript-estree@6.7.4(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 6.7.4
|
||||
'@typescript-eslint/visitor-keys': 6.7.4
|
||||
@@ -11485,75 +11469,75 @@ snapshots:
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
semver: 7.5.4
|
||||
ts-api-utils: 1.0.1(typescript@5.5.2)
|
||||
ts-api-utils: 1.0.1(typescript@6.0.2)
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.54.0(typescript@5.5.2)':
|
||||
'@typescript-eslint/typescript-estree@8.54.0(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.54.0(typescript@5.5.2)
|
||||
'@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.5.2)
|
||||
'@typescript-eslint/project-service': 8.54.0(typescript@6.0.2)
|
||||
'@typescript-eslint/tsconfig-utils': 8.54.0(typescript@6.0.2)
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/visitor-keys': 8.54.0
|
||||
debug: 4.4.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.3
|
||||
tinyglobby: 0.2.15
|
||||
ts-api-utils: 2.4.0(typescript@5.5.2)
|
||||
typescript: 5.5.2
|
||||
ts-api-utils: 2.4.0(typescript@6.0.2)
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/typescript-estree@8.55.0(typescript@5.5.2)':
|
||||
'@typescript-eslint/typescript-estree@8.55.0(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@typescript-eslint/project-service': 8.55.0(typescript@5.5.2)
|
||||
'@typescript-eslint/tsconfig-utils': 8.55.0(typescript@5.5.2)
|
||||
'@typescript-eslint/project-service': 8.55.0(typescript@6.0.2)
|
||||
'@typescript-eslint/tsconfig-utils': 8.55.0(typescript@6.0.2)
|
||||
'@typescript-eslint/types': 8.55.0
|
||||
'@typescript-eslint/visitor-keys': 8.55.0
|
||||
debug: 4.4.3
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.3
|
||||
tinyglobby: 0.2.15
|
||||
ts-api-utils: 2.4.0(typescript@5.5.2)
|
||||
typescript: 5.5.2
|
||||
ts-api-utils: 2.4.0(typescript@6.0.2)
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@6.7.4(eslint@8.50.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/utils@6.7.4(eslint@8.50.0)(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0)
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/semver': 7.5.3
|
||||
'@typescript-eslint/scope-manager': 6.7.4
|
||||
'@typescript-eslint/types': 6.7.4
|
||||
'@typescript-eslint/typescript-estree': 6.7.4(typescript@5.5.2)
|
||||
'@typescript-eslint/typescript-estree': 6.7.4(typescript@6.0.2)
|
||||
eslint: 8.50.0
|
||||
semver: 7.5.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@typescript-eslint/utils@8.54.0(eslint@8.50.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/utils@8.54.0(eslint@8.50.0)(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@8.50.0)
|
||||
'@typescript-eslint/scope-manager': 8.54.0
|
||||
'@typescript-eslint/types': 8.54.0
|
||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@5.5.2)
|
||||
'@typescript-eslint/typescript-estree': 8.54.0(typescript@6.0.2)
|
||||
eslint: 8.50.0
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@8.55.0(eslint@8.50.0)(typescript@5.5.2)':
|
||||
'@typescript-eslint/utils@8.55.0(eslint@8.50.0)(typescript@6.0.2)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@8.50.0)
|
||||
'@typescript-eslint/scope-manager': 8.55.0
|
||||
'@typescript-eslint/types': 8.55.0
|
||||
'@typescript-eslint/typescript-estree': 8.55.0(typescript@5.5.2)
|
||||
'@typescript-eslint/typescript-estree': 8.55.0(typescript@6.0.2)
|
||||
eslint: 8.50.0
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -11631,7 +11615,7 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(terser@5.44.1)(yaml@1.10.2))':
|
||||
'@vitest/coverage-v8@4.0.18(vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(terser@5.44.1)(yaml@1.10.2))':
|
||||
dependencies:
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
'@vitest/utils': 4.0.18
|
||||
@@ -11643,16 +11627,16 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 3.10.0
|
||||
tinyrainbow: 3.0.3
|
||||
vitest: 4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(terser@5.44.1)(yaml@1.10.2)
|
||||
vitest: 4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(terser@5.44.1)(yaml@1.10.2)
|
||||
|
||||
'@vitest/eslint-plugin@1.6.7(eslint@8.50.0)(typescript@5.5.2)(vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(terser@5.44.1)(yaml@1.10.2))':
|
||||
'@vitest/eslint-plugin@1.6.7(eslint@8.50.0)(typescript@6.0.2)(vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(terser@5.44.1)(yaml@1.10.2))':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 8.55.0
|
||||
'@typescript-eslint/utils': 8.55.0(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/utils': 8.55.0(eslint@8.50.0)(typescript@6.0.2)
|
||||
eslint: 8.50.0
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
vitest: 4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(terser@5.44.1)(yaml@1.10.2)
|
||||
typescript: 6.0.2
|
||||
vitest: 4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(terser@5.44.1)(yaml@1.10.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -11665,13 +11649,13 @@ snapshots:
|
||||
chai: 6.2.2
|
||||
tinyrainbow: 3.0.3
|
||||
|
||||
'@vitest/mocker@4.0.18(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(vite@7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))':
|
||||
'@vitest/mocker@4.0.18(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(vite@7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))':
|
||||
dependencies:
|
||||
'@vitest/spy': 4.0.18
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
msw: 2.12.10(@types/node@25.0.3)(typescript@5.5.2)
|
||||
msw: 2.12.10(@types/node@25.0.3)(typescript@6.0.2)
|
||||
vite: 7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)
|
||||
|
||||
'@vitest/pretty-format@4.0.18':
|
||||
@@ -12624,9 +12608,9 @@ snapshots:
|
||||
|
||||
crelt@1.0.6: {}
|
||||
|
||||
cross-fetch@3.1.5:
|
||||
cross-fetch@4.1.0:
|
||||
dependencies:
|
||||
node-fetch: 2.6.7
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
@@ -13194,24 +13178,24 @@ snapshots:
|
||||
dependencies:
|
||||
confusing-browser-globals: 1.0.11
|
||||
eslint: 8.50.0
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
object.assign: 4.1.4
|
||||
object.entries: 1.1.6
|
||||
semver: 6.3.1
|
||||
|
||||
eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint@8.50.0)(typescript@5.5.2))(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0):
|
||||
eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint@8.50.0)(typescript@6.0.2))(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0):
|
||||
dependencies:
|
||||
'@typescript-eslint/eslint-plugin': 6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/eslint-plugin': 6.7.4(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint@8.50.0)(typescript@6.0.2)
|
||||
'@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@6.0.2)
|
||||
eslint: 8.50.0
|
||||
eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.28.1)(eslint@8.50.0)
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
|
||||
eslint-config-airbnb@19.0.4(eslint-plugin-import@2.28.1)(eslint-plugin-jsx-a11y@6.7.1(eslint@8.50.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.50.0))(eslint-plugin-react@7.33.2(eslint@8.50.0))(eslint@8.50.0):
|
||||
dependencies:
|
||||
eslint: 8.50.0
|
||||
eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.28.1)(eslint@8.50.0)
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-plugin-jsx-a11y: 6.7.1(eslint@8.50.0)
|
||||
eslint-plugin-react: 7.33.2(eslint@8.50.0)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@8.50.0)
|
||||
@@ -13224,7 +13208,7 @@ snapshots:
|
||||
|
||||
eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.28.1):
|
||||
dependencies:
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
|
||||
eslint-import-resolver-node@0.3.7:
|
||||
dependencies:
|
||||
@@ -13234,13 +13218,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0):
|
||||
eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0):
|
||||
dependencies:
|
||||
debug: 4.3.7(supports-color@5.5.0)
|
||||
enhanced-resolve: 5.15.0
|
||||
eslint: 8.50.0
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-plugin-import: 2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
fast-glob: 3.3.2
|
||||
get-tsconfig: 4.7.2
|
||||
is-core-module: 2.13.0
|
||||
@@ -13251,14 +13235,14 @@ snapshots:
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0):
|
||||
eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@6.0.2)
|
||||
eslint: 8.50.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0)
|
||||
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-plugin-import@2.28.1)(eslint@8.50.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -13268,7 +13252,7 @@ snapshots:
|
||||
eslint: 8.50.0
|
||||
ignore: 5.2.4
|
||||
|
||||
eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0):
|
||||
eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0):
|
||||
dependencies:
|
||||
array-includes: 3.1.6
|
||||
array.prototype.findlastindex: 1.2.3
|
||||
@@ -13278,7 +13262,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.50.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@6.0.2))(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.6.1)(eslint@8.50.0)
|
||||
has: 1.0.3
|
||||
is-core-module: 2.13.0
|
||||
is-glob: 4.0.3
|
||||
@@ -13289,7 +13273,7 @@ snapshots:
|
||||
semver: 6.3.1
|
||||
tsconfig-paths: 3.14.2
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@6.0.2)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
@@ -13347,9 +13331,9 @@ snapshots:
|
||||
dependencies:
|
||||
eslint: 8.50.0
|
||||
|
||||
eslint-plugin-storybook@9.1.17(eslint@8.50.0)(storybook@8.6.15(prettier@3.0.3))(typescript@5.5.2):
|
||||
eslint-plugin-storybook@9.1.17(eslint@8.50.0)(storybook@8.6.15(prettier@3.0.3))(typescript@6.0.2):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@8.50.0)(typescript@5.5.2)
|
||||
'@typescript-eslint/utils': 8.54.0(eslint@8.50.0)(typescript@6.0.2)
|
||||
eslint: 8.50.0
|
||||
storybook: 8.6.15(prettier@3.0.3)
|
||||
transitivePeerDependencies:
|
||||
@@ -13620,7 +13604,7 @@ snapshots:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fork-ts-checker-webpack-plugin@8.0.0(typescript@5.5.2)(webpack@5.105.0):
|
||||
fork-ts-checker-webpack-plugin@8.0.0(typescript@6.0.2)(webpack@5.105.0):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
chalk: 4.1.2
|
||||
@@ -13634,7 +13618,7 @@ snapshots:
|
||||
schema-utils: 3.3.0
|
||||
semver: 7.7.3
|
||||
tapable: 2.3.0
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.21.5)(webpack-cli@6.0.1)
|
||||
|
||||
form-data@4.0.4:
|
||||
@@ -14018,9 +14002,9 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.0
|
||||
|
||||
i18next-http-backend@1.4.1:
|
||||
i18next-http-backend@3.0.4:
|
||||
dependencies:
|
||||
cross-fetch: 3.1.5
|
||||
cross-fetch: 4.1.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
@@ -14730,12 +14714,12 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
msw-storybook-addon@2.0.6(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2)):
|
||||
msw-storybook-addon@2.0.6(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2)):
|
||||
dependencies:
|
||||
is-node-process: 1.2.0
|
||||
msw: 2.12.10(@types/node@25.0.3)(typescript@5.5.2)
|
||||
msw: 2.12.10(@types/node@25.0.3)(typescript@6.0.2)
|
||||
|
||||
msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2):
|
||||
msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2):
|
||||
dependencies:
|
||||
'@inquirer/confirm': 5.1.21(@types/node@25.0.3)
|
||||
'@mswjs/interceptors': 0.41.2
|
||||
@@ -14756,7 +14740,7 @@ snapshots:
|
||||
until-async: 3.0.2
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
|
||||
@@ -14818,10 +14802,6 @@ snapshots:
|
||||
dependencies:
|
||||
http2-client: 1.3.5
|
||||
|
||||
node-fetch@2.6.7:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
@@ -15535,23 +15515,23 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/react-dom'
|
||||
|
||||
react-docgen-typescript-plugin@1.0.5(typescript@5.5.2)(webpack@5.105.0):
|
||||
react-docgen-typescript-plugin@1.0.5(typescript@6.0.2)(webpack@5.105.0):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
endent: 2.1.0
|
||||
find-cache-dir: 3.3.2
|
||||
flat-cache: 3.0.4
|
||||
micromatch: 4.0.8
|
||||
react-docgen-typescript: 2.2.2(typescript@5.5.2)
|
||||
react-docgen-typescript: 2.2.2(typescript@6.0.2)
|
||||
tslib: 2.8.1
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
webpack: 5.105.0(@swc/core@1.15.11)(esbuild@0.21.5)(webpack-cli@6.0.1)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react-docgen-typescript@2.2.2(typescript@5.5.2):
|
||||
react-docgen-typescript@2.2.2(typescript@6.0.2):
|
||||
dependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
|
||||
react-docgen@7.0.1:
|
||||
dependencies:
|
||||
@@ -16597,13 +16577,13 @@ snapshots:
|
||||
|
||||
try@1.0.3: {}
|
||||
|
||||
ts-api-utils@1.0.1(typescript@5.5.2):
|
||||
ts-api-utils@1.0.1(typescript@6.0.2):
|
||||
dependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
|
||||
ts-api-utils@2.4.0(typescript@5.5.2):
|
||||
ts-api-utils@2.4.0(typescript@6.0.2):
|
||||
dependencies:
|
||||
typescript: 5.5.2
|
||||
typescript: 6.0.2
|
||||
|
||||
ts-dedent@2.2.0: {}
|
||||
|
||||
@@ -16611,15 +16591,9 @@ snapshots:
|
||||
|
||||
ts-xor@1.1.0: {}
|
||||
|
||||
tsconfck@3.0.1(typescript@5.5.2):
|
||||
tsconfck@3.0.1(typescript@6.0.2):
|
||||
optionalDependencies:
|
||||
typescript: 5.5.2
|
||||
|
||||
tsconfig-paths-webpack-plugin@4.1.0:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
enhanced-resolve: 5.15.0
|
||||
tsconfig-paths: 4.2.0
|
||||
typescript: 6.0.2
|
||||
|
||||
tsconfig-paths@3.14.2:
|
||||
dependencies:
|
||||
@@ -16690,7 +16664,7 @@ snapshots:
|
||||
for-each: 0.3.3
|
||||
is-typed-array: 1.1.12
|
||||
|
||||
typescript@5.5.2: {}
|
||||
typescript@6.0.2: {}
|
||||
|
||||
uc.micro@1.0.6: {}
|
||||
|
||||
@@ -16870,11 +16844,11 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-tsconfig-paths@4.3.1(typescript@5.5.2)(vite@7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)):
|
||||
vite-tsconfig-paths@4.3.1(typescript@6.0.2)(vite@7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.0.1(typescript@5.5.2)
|
||||
tsconfck: 3.0.1(typescript@6.0.2)
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2)
|
||||
transitivePeerDependencies:
|
||||
@@ -16896,10 +16870,10 @@ snapshots:
|
||||
terser: 5.44.1
|
||||
yaml: 1.10.2
|
||||
|
||||
vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(terser@5.44.1)(yaml@1.10.2):
|
||||
vitest@4.0.18(@types/node@25.0.3)(jsdom@24.1.3)(less@4.4.2)(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(terser@5.44.1)(yaml@1.10.2):
|
||||
dependencies:
|
||||
'@vitest/expect': 4.0.18
|
||||
'@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@25.0.3)(typescript@5.5.2))(vite@7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))
|
||||
'@vitest/mocker': 4.0.18(msw@2.12.10(@types/node@25.0.3)(typescript@6.0.2))(vite@7.3.1(@types/node@25.0.3)(less@4.4.2)(terser@5.44.1)(yaml@1.10.2))
|
||||
'@vitest/pretty-format': 4.0.18
|
||||
'@vitest/runner': 4.0.18
|
||||
'@vitest/snapshot': 4.0.18
|
||||
|
||||
+9
-15
@@ -1,10 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "app",
|
||||
"outDir": "./dist/public",
|
||||
"module": "es6",
|
||||
// "module": "commonjs",
|
||||
// "module": "esnext",
|
||||
"incremental": true,
|
||||
"target": "esnext",
|
||||
"allowJs": true,
|
||||
@@ -15,7 +12,7 @@
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"noEmit": false,
|
||||
"jsx": "react-jsx",
|
||||
@@ -26,19 +23,16 @@
|
||||
"noUnusedLocals": true,
|
||||
"removeComments": true,
|
||||
"resolveJsonModule": true,
|
||||
// "sourceMap": true,
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"paths": {
|
||||
// paths relative to the baseUrl
|
||||
"@@/*": ["react/components/*"],
|
||||
"@/*": ["./*", "../app/*"],
|
||||
"Agent/*": ["agent/*"],
|
||||
"Azure/*": ["azure/*"],
|
||||
"Docker/*": ["docker/*"],
|
||||
"Kubernetes/*": ["kubernetes/*"],
|
||||
"Portainer/*": ["portainer/*"],
|
||||
// https://github.com/jsonnext/codemirror-json-schema/issues/107#issuecomment-2144584296
|
||||
"yaml-schema": ["../node_modules/codemirror-json-schema/dist/yaml"]
|
||||
"@@/*": ["./app/react/components/*"],
|
||||
"@/*": ["./app/*"],
|
||||
"Agent/*": ["./app/agent/*"],
|
||||
"Azure/*": ["./app/azure/*"],
|
||||
"Docker/*": ["./app/docker/*"],
|
||||
"Kubernetes/*": ["./app/kubernetes/*"],
|
||||
"Portainer/*": ["./app/portainer/*"],
|
||||
"yaml-schema": ["./node_modules/codemirror-json-schema/dist/yaml"]
|
||||
},
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom", "node"]
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ const WebpackBuildNotifierPlugin = require('webpack-build-notifier');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
const Dotenv = require('dotenv-webpack');
|
||||
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
@@ -26,7 +25,6 @@ module.exports = {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
type: 'javascript/auto',
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
use: ['source-map-loader'],
|
||||
@@ -163,6 +161,7 @@ module.exports = {
|
||||
shorthands: true,
|
||||
collections: true,
|
||||
paths: true,
|
||||
flattening: true,
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
@@ -212,10 +211,6 @@ module.exports = {
|
||||
'yaml-schema': path.resolve(projectRoot, 'node_modules/codemirror-json-schema/dist/yaml'),
|
||||
},
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
plugins: [
|
||||
new TsconfigPathsPlugin({
|
||||
extensions: ['.js', '.ts', '.tsx'],
|
||||
}),
|
||||
],
|
||||
tsconfig: path.resolve(projectRoot, 'tsconfig.json'),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user