Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.24.13
require (
github.com/Masterminds/semver/v3 v3.3.1
github.com/deckhouse/deckhouse/pkg/log v0.2.0
github.com/deckhouse/deckhouse/pkg/registry v0.0.0-20260326143935-b535ad6cb730
github.com/deckhouse/deckhouse/pkg/registry v0.0.0-20260407161812-f9afe0918c6e
github.com/deckhouse/virtualization/src/cli v1.5.1
github.com/fatih/color v1.18.0
github.com/fluxcd/flagger v1.36.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,8 @@ github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80N
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/deckhouse/deckhouse/pkg/log v0.2.0 h1:6tmZQLwNb1o/hP1gzJQBjcwfA/bubbgObovXzxq+Exo=
github.com/deckhouse/deckhouse/pkg/log v0.2.0/go.mod h1:pbAxTSDcPmwyl3wwKDcEB3qdxHnRxqTV+J0K+sha8bw=
github.com/deckhouse/deckhouse/pkg/registry v0.0.0-20260326143935-b535ad6cb730 h1:kJs7prT4Aq/Ek5j5LO0dmIytTSPuMqfxpYK5L3tDYR0=
github.com/deckhouse/deckhouse/pkg/registry v0.0.0-20260326143935-b535ad6cb730/go.mod h1:KDf44MqEif8jAKCehKJqOg0k4sJcnetKJKDGd0IFQjI=
github.com/deckhouse/deckhouse/pkg/registry v0.0.0-20260407161812-f9afe0918c6e h1:+gC4s7xV/zK3q1Qlw69Bz95j8vHU3xFBD6CouP3x+eU=
github.com/deckhouse/deckhouse/pkg/registry v0.0.0-20260407161812-f9afe0918c6e/go.mod h1:KDf44MqEif8jAKCehKJqOg0k4sJcnetKJKDGd0IFQjI=
github.com/deckhouse/delivery-kit-sdk v1.0.1-0.20251022121655-0cbac8223333 h1:9YuKuSrRfTSdAaLIUwi0t4bkedoNgj3IMFXPcP/rkSo=
github.com/deckhouse/delivery-kit-sdk v1.0.1-0.20251022121655-0cbac8223333/go.mod h1:iukQB9dt5DasWCI/XkjeyD5KtTJtHp5DMCGN8IS5PqA=
github.com/deckhouse/delivery-kit/v2 v2.63.1-dk h1:1YDVwEJMoEfiNGdEuiuXIfyJ3Ux8evnCsmJYZrJBmHU=
Expand Down
5 changes: 5 additions & 0 deletions internal/mirror/cmd/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"github.com/deckhouse/deckhouse-cli/internal/mirror/gostsums"
"github.com/deckhouse/deckhouse-cli/internal/mirror/modules"
"github.com/deckhouse/deckhouse-cli/internal/version"
"github.com/deckhouse/deckhouse-cli/pkg"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/operations/params"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/validation"
Expand Down Expand Up @@ -219,6 +220,7 @@ func NewPuller(cmd *cobra.Command) *Puller {
},
}
}

func (p *Puller) Execute(ctx context.Context) error {
if err := p.cleanupWorkingDirectory(); err != nil {
return err
Expand Down Expand Up @@ -253,6 +255,9 @@ func (p *Puller) Execute(ctx context.Context) error {

if os.Getenv("STUB_REGISTRY_CLIENT") == "true" {
c = stub.NewRegistryClientStub()
// The stub's root URL already includes the edition path segment, so we
// must not add it again via registryservice.NewService.
edition = pkg.NoEdition
}

// Scope to the registry path and modules suffix
Expand Down
187 changes: 187 additions & 0 deletions internal/mirror/modules/validate_access_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
Copyright 2026 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package modules

import (
"context"
"log/slog"
"testing"

"github.com/stretchr/testify/require"

dkplog "github.com/deckhouse/deckhouse/pkg/log"

"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log"
registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service"
"github.com/deckhouse/deckhouse-cli/pkg/stub"
)

func TestService_validateModulesAccess(t *testing.T) {
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

t.Run("modules present in registry returns no error", func(t *testing.T) {
// Build a stub where the "modules" repository has module names as tags.
reg := stub.NewRegistry("registry.example.com")
placeholder := stub.NewImageBuilder().MustBuild()
reg.MustAddImage("modules", "console", placeholder)
reg.MustAddImage("modules", "ingress-nginx", placeholder)
reg.MustAddImage("modules", "cert-manager", placeholder)

stubClient := stub.NewClient(reg)
// Scope client to "modules" so that ListTags returns module names.
modulesClient := stubClient.WithSegment("modules")
modulesService := registryservice.NewModulesService(modulesClient, logger)

svc := &Service{
modulesService: modulesService,
options: &Options{},
logger: logger,
userLogger: userLogger,
}

err := svc.validateModulesAccess(context.Background())
require.NoError(t, err)
})

t.Run("modules repository absent in registry returns no error and emits warning", func(t *testing.T) {
// Empty registry – the "modules" repo does not exist, so ListTags returns
// ErrImageNotFound which validateModulesAccess treats as a graceful skip.
reg := stub.NewRegistry("registry.example.com")
stubClient := stub.NewClient(reg)
modulesClient := stubClient.WithSegment("modules")
modulesService := registryservice.NewModulesService(modulesClient, logger)

svc := &Service{
modulesService: modulesService,
options: &Options{},
logger: logger,
userLogger: userLogger,
}

err := svc.validateModulesAccess(context.Background())
require.NoError(t, err)
})
}

func TestService_validateModulesAccess_WithFilter(t *testing.T) {
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

reg := stub.NewRegistry("registry.example.com")
placeholder := stub.NewImageBuilder().MustBuild()
reg.MustAddImage("modules", "console", placeholder)
reg.MustAddImage("modules", "ingress-nginx", placeholder)
reg.MustAddImage("modules", "sds-replicated-volume", placeholder)

stubClient := stub.NewClient(reg)
modulesClient := stubClient.WithSegment("modules")
modulesService := registryservice.NewModulesService(modulesClient, logger)

// validateModulesAccess does not use the filter; it only checks reachability.
// The filter is applied later in pullModules. We verify here that any valid
// filter configuration does not affect the access check result.
tests := []struct {
name string
filter *Filter
}{
{
name: "whitelist subset of modules",
filter: func() *Filter {
f, _ := NewFilter([]string{"console", "ingress-nginx"}, FilterTypeWhitelist)
return f
}(),
},
{
name: "blacklist single module",
filter: func() *Filter {
f, _ := NewFilter([]string{"sds-replicated-volume"}, FilterTypeBlacklist)
return f
}(),
},
{
name: "empty blacklist accepts all",
filter: func() *Filter {
f, _ := NewFilter(nil, FilterTypeBlacklist)
return f
}(),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := &Service{
modulesService: modulesService,
options: &Options{Filter: tt.filter},
logger: logger,
userLogger: userLogger,
}

err := svc.validateModulesAccess(context.Background())
require.NoError(t, err)
})
}
}

func TestService_validateModulesAccess_Timeout(t *testing.T) {
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

reg := stub.NewRegistry("registry.example.com")
placeholder := stub.NewImageBuilder().MustBuild()
reg.MustAddImage("modules", "console", placeholder)

stubClient := stub.NewClient(reg)
modulesClient := stubClient.WithSegment("modules")
modulesService := registryservice.NewModulesService(modulesClient, logger)

// Even with a Timeout option set, validateModulesAccess should succeed
// (the timeout is applied at the network level, not in the stub).
svc := &Service{
modulesService: modulesService,
options: &Options{},
logger: logger,
userLogger: userLogger,
}

err := svc.validateModulesAccess(context.Background())
require.NoError(t, err)
}

// TestModule_Versions verifies that Module.Versions() correctly parses semver
// release tags and ignores non-semver strings (such as channel names).
func TestModule_Versions(t *testing.T) {
_ = log.NewSLogger(slog.LevelDebug) // silence unused-import lint

mod := &Module{
Name: "console",
Releases: []string{
"alpha", "beta", "early-access", "stable", "rock-solid", // non-semver
"v1.0.0", "v1.1.0", "v1.2.3", // semver
"notasemver",
},
}

versions := mod.Versions()
got := make([]string, 0, len(versions))
for _, v := range versions {
got = append(got, "v"+v.String())
}

want := []string{"v1.0.0", "v1.1.0", "v1.2.3"}
require.ElementsMatch(t, want, got)
}
54 changes: 19 additions & 35 deletions internal/mirror/platform/platform_dryrun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,23 @@ import (

dkplog "github.com/deckhouse/deckhouse/pkg/log"

"github.com/deckhouse/deckhouse-cli/pkg"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log"
registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service"
"github.com/deckhouse/deckhouse-cli/pkg/stub"
)

// TestDryRun_NoBundleFilesWritten verifies that PullPlatform in dry-run mode does
// not write any files to the bundle directory. Temporary OCI layout data may only
// land under the working/tmp directory.
// not write any files to the bundle directory.
func TestDryRun_NoBundleFilesWritten(t *testing.T) {
workingDir := t.TempDir()
bundleDir := t.TempDir() // must stay empty after dry-run

stubClient := stub.NewRegistryClientStub()
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

regSvc := registryservice.NewService(stubClient, pkg.FEEdition, logger)

svc := NewService(
regSvc,
workingDir,
&Options{
TargetTag: "v1.69.0",
BundleDir: bundleDir,
DryRun: true,
},
svc := newTestPlatformService(
stubClient,
&Options{TargetTag: "v1.69.0", BundleDir: bundleDir, DryRun: true},
logger,
userLogger,
)
Expand All @@ -68,40 +58,34 @@ func TestDryRun_NoBundleFilesWritten(t *testing.T) {
assert.Empty(t, entries, "dry-run must not write any files to the bundle directory; found: %v", entries)
}

// TestDryRun_InstallerPulledToTmpDir verifies that in dry-run mode the installer
// image IS pulled into the working (tmp) directory so that images_digests.json can
// be read from it. This produces the complete list of images that would be
// downloaded in a real run.
func TestDryRun_InstallerPulledToTmpDir(t *testing.T) {
// TestDryRun_NoOCILayoutCreated verifies that in dry-run mode no OCI image layout
// directories are created under the working directory. images_digests.json is
// streamed directly from the remote registry without writing anything to disk.
func TestDryRun_NoOCILayoutCreated(t *testing.T) {
workingDir := t.TempDir()
bundleDir := t.TempDir()

stubClient := stub.NewRegistryClientStub()
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

regSvc := registryservice.NewService(stubClient, pkg.FEEdition, logger)

svc := NewService(
regSvc,
workingDir,
&Options{
TargetTag: "v1.69.0",
BundleDir: bundleDir,
DryRun: true,
},
logger,
userLogger,
)
deckhouseSvc := registryservice.NewDeckhouseService(stubClient, logger)
svc := &Service{
deckhouseService: deckhouseSvc,
downloadList: NewImageDownloadList(stubClient.GetRegistry()),
options: &Options{TargetTag: "v1.69.0", BundleDir: bundleDir, DryRun: true},
logger: logger,
userLogger: userLogger,
}

err := svc.PullPlatform(context.Background())
require.NoError(t, err)

// In optimized dry-run, no OCI layout is created - images_digests.json is
// streamed directly from the remote registry via ExtractFileFromImage.
// No OCI layout directories should be created.
installerLayoutDir := filepath.Join(workingDir, "platform", "install")
_, statErr := os.Stat(installerLayoutDir)
assert.ErrorIs(t, statErr, os.ErrNotExist, "installer OCI layout must NOT be created in dry-run; dir: %s", installerLayoutDir)
assert.ErrorIs(t, statErr, os.ErrNotExist,
"installer OCI layout must NOT be created in dry-run")

// bundleDir must remain empty
entries, err := os.ReadDir(bundleDir)
Expand Down
Loading
Loading