From 0724faa117630ad819d8e7dc40772a5a4d969c50 Mon Sep 17 00:00:00 2001 From: Roman Berezkin Date: Mon, 20 Apr 2026 17:47:22 +0300 Subject: [PATCH] [deckhouse-cli] Fix missing edition segment in mirror pull image refs + remove dead code - Security, platform, and modules pullers build image refs from the edition-scoped registry root, so logs and OCI `ref.name` annotations in pulled archives include the correct edition path (e.g. `/deckhouse/ee/security/trivy-db:2`). - Clarify docstrings on the registry service tree so it's clear how sub-services map to paths under the chosen base. - Drop unreachable helpers in modules layout (ModulesDownloadList.Module, FillModulesImages, FillForTag). Signed-off-by: Roman Berezkin --- internal/mirror/installer/installer.go | 12 ++-- internal/mirror/modules/layout.go | 39 +++--------- internal/mirror/modules/modules.go | 2 +- internal/mirror/modules/root_scope_test.go | 64 ++++++++++++++++++++ internal/mirror/platform/layout.go | 23 ++++--- internal/mirror/platform/platform.go | 2 +- internal/mirror/platform/root_scope_test.go | 63 +++++++++++++++++++ internal/mirror/puller/types.go | 5 +- internal/mirror/security/layout.go | 18 ++++-- internal/mirror/security/root_scope_test.go | 63 +++++++++++++++++++ internal/mirror/security/security.go | 2 +- pkg/registry/service/deckhouse_service.go | 10 ++- pkg/registry/service/service.go | 9 ++- pkg/registry/service/service_root_test.go | 67 +++++++++++++++++++++ 14 files changed, 323 insertions(+), 56 deletions(-) create mode 100644 internal/mirror/modules/root_scope_test.go create mode 100644 internal/mirror/platform/root_scope_test.go create mode 100644 internal/mirror/security/root_scope_test.go create mode 100644 pkg/registry/service/service_root_test.go diff --git a/internal/mirror/installer/installer.go b/internal/mirror/installer/installer.go index 116e40ab..328ffd65 100644 --- a/internal/mirror/installer/installer.go +++ b/internal/mirror/installer/installer.go @@ -95,11 +95,13 @@ func NewService( return &Service{ registryService: registryService, layout: layout, - downloadList: NewImageDownloadList(registryService.GetRoot()), - pullerService: puller.NewPullerService(logger, userLogger), - options: options, - logger: logger, - userLogger: userLogger, + // GetRoot() is edition-agnostic (e.g. /deckhouse); the installer + // lives at /installer:, outside the edition segment. + downloadList: NewImageDownloadList(registryService.GetRoot()), + pullerService: puller.NewPullerService(logger, userLogger), + options: options, + logger: logger, + userLogger: userLogger, } } diff --git a/internal/mirror/modules/layout.go b/internal/mirror/modules/layout.go index 2fc853a4..20ea6447 100644 --- a/internal/mirror/modules/layout.go +++ b/internal/mirror/modules/layout.go @@ -28,6 +28,7 @@ import ( regimage "github.com/deckhouse/deckhouse-cli/pkg/registry/image" ) +// ModulesDownloadList is a to-do list grouped by module name. type ModulesDownloadList struct { rootURL string list map[string]*ImageDownloadList @@ -40,44 +41,24 @@ func NewModulesDownloadList(rootURL string) *ModulesDownloadList { } } -func (l *ModulesDownloadList) Module(moduleName string) *ImageDownloadList { - return l.list[moduleName] -} - -func (l *ModulesDownloadList) FillModulesImages(modules []string) { - for _, moduleName := range modules { - list := NewImageDownloadList(filepath.Join(l.rootURL, moduleName)) - list.FillForTag("") - l.list[moduleName] = list - } -} - +// ImageDownloadList queues image refs for a single module for the puller. +// - Values: nil until the puller fills them with metadata. +// - rootURL: module-scoped path, used to build refs for display, not HTTP. type ImageDownloadList struct { rootURL string - Module map[string]*puller.ImageMeta - ModuleReleaseChannels map[string]*puller.ImageMeta - ModuleExtra map[string]*puller.ImageMeta + Module map[puller.ImageRef]*puller.ImageMeta + ModuleReleaseChannels map[puller.ImageRef]*puller.ImageMeta + ModuleExtra map[puller.ImageRef]*puller.ImageMeta } func NewImageDownloadList(rootURL string) *ImageDownloadList { return &ImageDownloadList{ rootURL: rootURL, - Module: make(map[string]*puller.ImageMeta), - ModuleReleaseChannels: make(map[string]*puller.ImageMeta), - ModuleExtra: make(map[string]*puller.ImageMeta), - } -} - -func (l *ImageDownloadList) FillForTag(tag string) { - // If we are to pull only the specific requested version, we should not pull any release channels at all. - if tag != "" { - return - } - - for _, channel := range internal.GetAllDefaultReleaseChannels() { - l.ModuleReleaseChannels[l.rootURL+":"+channel] = nil + Module: make(map[puller.ImageRef]*puller.ImageMeta), + ModuleReleaseChannels: make(map[puller.ImageRef]*puller.ImageMeta), + ModuleExtra: make(map[puller.ImageRef]*puller.ImageMeta), } } diff --git a/internal/mirror/modules/modules.go b/internal/mirror/modules/modules.go index cd4e9b35..20ac0f6b 100644 --- a/internal/mirror/modules/modules.go +++ b/internal/mirror/modules/modules.go @@ -121,7 +121,7 @@ func NewService( options.Filter = filter } - rootURL := registryService.GetRoot() + rootURL := registryService.DeckhouseService().GetRoot() return &Service{ workingDir: workingDir, diff --git a/internal/mirror/modules/root_scope_test.go b/internal/mirror/modules/root_scope_test.go new file mode 100644 index 00000000..54b077cd --- /dev/null +++ b/internal/mirror/modules/root_scope_test.go @@ -0,0 +1,64 @@ +/* +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 ( + "log/slog" + "testing" + + "github.com/stretchr/testify/require" + + dkplog "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/deckhouse-cli/pkg" + "github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log" + registryclient "github.com/deckhouse/deckhouse-cli/pkg/registry/client" + registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service" +) + +func TestNewService_UsesDeckhouseRoot(t *testing.T) { + tests := []struct { + name string + edition pkg.Edition + wantRootURL string + }{ + { + name: "edition layout uses edition-scoped root", + edition: pkg.FEEdition, + wantRootURL: "registry.example.com/deckhouse/fe", + }, + { + name: "flat layout uses root without edition", + edition: pkg.NoEdition, + wantRootURL: "registry.example.com/deckhouse", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn)) + userLogger := log.NewSLogger(slog.LevelWarn) + client := registryclient.NewFromOptions("registry.example.com/deckhouse") + + regSvc := registryservice.NewService(client, tc.edition, logger) + svc := NewService(regSvc, t.TempDir(), &Options{DryRun: true}, logger, userLogger) + + require.Equal(t, tc.wantRootURL, svc.rootURL) + require.Equal(t, tc.wantRootURL, svc.modulesDownloadList.rootURL) + }) + } +} diff --git a/internal/mirror/platform/layout.go b/internal/mirror/platform/layout.go index a70a5f1f..689388f5 100644 --- a/internal/mirror/platform/layout.go +++ b/internal/mirror/platform/layout.go @@ -29,25 +29,28 @@ import ( regimage "github.com/deckhouse/deckhouse-cli/pkg/registry/image" ) +// ImageDownloadList queues image refs for the puller. +// - Values: nil until the puller fills them with metadata. +// - rootURL: used to build refs for display (logs, annotations), not HTTP. type ImageDownloadList struct { rootURL string - Deckhouse map[string]*puller.ImageMeta - DeckhouseExtra map[string]*puller.ImageMeta - DeckhouseInstall map[string]*puller.ImageMeta - DeckhouseInstallStandalone map[string]*puller.ImageMeta - DeckhouseReleaseChannel map[string]*puller.ImageMeta + Deckhouse map[puller.ImageRef]*puller.ImageMeta + DeckhouseExtra map[puller.ImageRef]*puller.ImageMeta + DeckhouseInstall map[puller.ImageRef]*puller.ImageMeta + DeckhouseInstallStandalone map[puller.ImageRef]*puller.ImageMeta + DeckhouseReleaseChannel map[puller.ImageRef]*puller.ImageMeta } func NewImageDownloadList(rootURL string) *ImageDownloadList { return &ImageDownloadList{ rootURL: rootURL, - Deckhouse: make(map[string]*puller.ImageMeta), - DeckhouseExtra: make(map[string]*puller.ImageMeta), - DeckhouseInstall: make(map[string]*puller.ImageMeta), - DeckhouseInstallStandalone: make(map[string]*puller.ImageMeta), - DeckhouseReleaseChannel: make(map[string]*puller.ImageMeta), + Deckhouse: make(map[puller.ImageRef]*puller.ImageMeta), + DeckhouseExtra: make(map[puller.ImageRef]*puller.ImageMeta), + DeckhouseInstall: make(map[puller.ImageRef]*puller.ImageMeta), + DeckhouseInstallStandalone: make(map[puller.ImageRef]*puller.ImageMeta), + DeckhouseReleaseChannel: make(map[puller.ImageRef]*puller.ImageMeta), } } diff --git a/internal/mirror/platform/platform.go b/internal/mirror/platform/platform.go index 1a6147f9..e4c7802d 100644 --- a/internal/mirror/platform/platform.go +++ b/internal/mirror/platform/platform.go @@ -115,7 +115,7 @@ func NewService( } } - rootURL := registryService.GetRoot() + rootURL := registryService.DeckhouseService().GetRoot() return &Service{ deckhouseService: registryService.DeckhouseService(), diff --git a/internal/mirror/platform/root_scope_test.go b/internal/mirror/platform/root_scope_test.go new file mode 100644 index 00000000..e42b346b --- /dev/null +++ b/internal/mirror/platform/root_scope_test.go @@ -0,0 +1,63 @@ +/* +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 platform + +import ( + "log/slog" + "testing" + + "github.com/stretchr/testify/require" + + dkplog "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/deckhouse-cli/pkg" + "github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log" + registryclient "github.com/deckhouse/deckhouse-cli/pkg/registry/client" + registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service" +) + +func TestNewService_UsesDeckhouseRoot(t *testing.T) { + tests := []struct { + name string + edition pkg.Edition + wantRootURL string + }{ + { + name: "edition layout uses edition-scoped root", + edition: pkg.FEEdition, + wantRootURL: "registry.example.com/deckhouse/fe", + }, + { + name: "flat layout uses root without edition", + edition: pkg.NoEdition, + wantRootURL: "registry.example.com/deckhouse", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn)) + userLogger := log.NewSLogger(slog.LevelWarn) + client := registryclient.NewFromOptions("registry.example.com/deckhouse") + + regSvc := registryservice.NewService(client, tc.edition, logger) + svc := NewService(regSvc, t.TempDir(), &Options{DryRun: true}, logger, userLogger) + + require.Equal(t, tc.wantRootURL, svc.downloadList.rootURL) + }) + } +} diff --git a/internal/mirror/puller/types.go b/internal/mirror/puller/types.go index 2e85c7da..405e6a6f 100644 --- a/internal/mirror/puller/types.go +++ b/internal/mirror/puller/types.go @@ -28,13 +28,16 @@ import ( regimage "github.com/deckhouse/deckhouse-cli/pkg/registry/image" ) +// ImageRef is a full image reference (e.g. registry.example.com/repo:tag). +type ImageRef = string + // ImageGetter is a function type for getting images from the registry type ImageGetter func(ctx context.Context, tag string, opts ...registry.ImageGetOption) (pkg.RegistryImage, error) // PullConfig encapsulates the configuration for pulling images type PullConfig struct { Name string - ImageSet map[string]*ImageMeta + ImageSet map[ImageRef]*ImageMeta Layout *regimage.ImageLayout AllowMissingTags bool GetterService pkg.BasicService diff --git a/internal/mirror/security/layout.go b/internal/mirror/security/layout.go index 73e064d8..76d76b43 100644 --- a/internal/mirror/security/layout.go +++ b/internal/mirror/security/layout.go @@ -29,21 +29,27 @@ import ( regimage "github.com/deckhouse/deckhouse-cli/pkg/registry/image" ) +// DatabaseName identifies a security database (trivy-db, trivy-bdu, ...). +type DatabaseName = string + +// ImageDownloadList queues security database images for the puller. +// - Inner values: nil until the puller fills them with metadata. +// - rootURL: used to build refs for display (logs, annotations), not HTTP. type ImageDownloadList struct { rootURL string - Security map[string]map[string]*puller.ImageMeta + Security map[DatabaseName]map[puller.ImageRef]*puller.ImageMeta } func NewImageDownloadList(rootURL string) *ImageDownloadList { return &ImageDownloadList{ rootURL: rootURL, - Security: make(map[string]map[string]*puller.ImageMeta), + Security: make(map[DatabaseName]map[puller.ImageRef]*puller.ImageMeta), } } func (l *ImageDownloadList) FillSecurityImages() { - imageReferences := map[string]string{ + imageReferences := map[DatabaseName]puller.ImageRef{ internal.SecurityTrivyDBSegment: path.Join(l.rootURL, internal.SecuritySegment, internal.SecurityTrivyDBSegment) + ":2", internal.SecurityTrivyBDUSegment: path.Join(l.rootURL, internal.SecuritySegment, internal.SecurityTrivyBDUSegment) + ":1", internal.SecurityTrivyJavaDBSegment: path.Join(l.rootURL, internal.SecuritySegment, internal.SecurityTrivyJavaDBSegment) + ":1", @@ -51,7 +57,7 @@ func (l *ImageDownloadList) FillSecurityImages() { } for name, ref := range imageReferences { - l.Security[name] = map[string]*puller.ImageMeta{ + l.Security[name] = map[puller.ImageRef]*puller.ImageMeta{ ref: nil, } } @@ -61,14 +67,14 @@ type ImageLayouts struct { platform v1.Platform workingDir string - Security map[string]*regimage.ImageLayout + Security map[DatabaseName]*regimage.ImageLayout } func NewImageLayouts(rootFolder string) *ImageLayouts { l := &ImageLayouts{ workingDir: rootFolder, platform: v1.Platform{Architecture: "amd64", OS: "linux"}, - Security: make(map[string]*regimage.ImageLayout, 1), + Security: make(map[DatabaseName]*regimage.ImageLayout, 1), } return l diff --git a/internal/mirror/security/root_scope_test.go b/internal/mirror/security/root_scope_test.go new file mode 100644 index 00000000..8dec51ae --- /dev/null +++ b/internal/mirror/security/root_scope_test.go @@ -0,0 +1,63 @@ +/* +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 security + +import ( + "log/slog" + "testing" + + "github.com/stretchr/testify/require" + + dkplog "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/deckhouse-cli/pkg" + "github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log" + registryclient "github.com/deckhouse/deckhouse-cli/pkg/registry/client" + registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service" +) + +func TestNewService_UsesDeckhouseRoot(t *testing.T) { + tests := []struct { + name string + edition pkg.Edition + wantRootURL string + }{ + { + name: "edition layout uses edition-scoped root", + edition: pkg.FEEdition, + wantRootURL: "registry.example.com/deckhouse/fe", + }, + { + name: "flat layout uses root without edition", + edition: pkg.NoEdition, + wantRootURL: "registry.example.com/deckhouse", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn)) + userLogger := log.NewSLogger(slog.LevelWarn) + client := registryclient.NewFromOptions("registry.example.com/deckhouse") + + regSvc := registryservice.NewService(client, tc.edition, logger) + svc := NewService(regSvc, t.TempDir(), &Options{DryRun: true}, logger, userLogger) + + require.Equal(t, tc.wantRootURL, svc.downloadList.rootURL) + }) + } +} diff --git a/internal/mirror/security/security.go b/internal/mirror/security/security.go index 93819cf8..5d1b0c30 100644 --- a/internal/mirror/security/security.go +++ b/internal/mirror/security/security.go @@ -92,7 +92,7 @@ func NewService( return &Service{ securityService: registryService.Security(), layout: layout, - downloadList: NewImageDownloadList(registryService.GetRoot()), + downloadList: NewImageDownloadList(registryService.DeckhouseService().GetRoot()), pullerService: puller.NewPullerService(logger, userLogger), options: options, logger: logger, diff --git a/pkg/registry/service/deckhouse_service.go b/pkg/registry/service/deckhouse_service.go index 7bf65b1c..daf15366 100644 --- a/pkg/registry/service/deckhouse_service.go +++ b/pkg/registry/service/deckhouse_service.go @@ -39,7 +39,15 @@ const ( standaloneInstallerServiceName = "standalone_installer" ) -// DeckhouseService provides high-level operations for Deckhouse platform management +// DeckhouseService is the root for platform images in registry. +// Its client points to the deckhouse base chosen by Service.NewService +// (with edition segment for upstream, without it for flat mirrors). +// Sub-services provide access to paths under that base: +// +// (self) -> (deckhouse main image) +// ReleaseChannels() -> /release-channel +// Installer() -> /install +// StandaloneInstaller() -> /install-standalone type DeckhouseService struct { client client.Client diff --git a/pkg/registry/service/service.go b/pkg/registry/service/service.go index f9d34be4..ce136a17 100644 --- a/pkg/registry/service/service.go +++ b/pkg/registry/service/service.go @@ -62,11 +62,18 @@ func NewService(c client.Client, edition pkg.Edition, logger *log.Logger) *Servi base = c.WithSegment(edition.String()) } + // Edition-scoped services (built from base; see branches above). + // modules -> /modules + // deckhouse -> (platform images, release-channel, install, etc.) + // security -> /security + // For upstream = ".../deckhouse/ee"; for flat mirror = ".../deckhouse". s.modulesService = NewModulesService(base.WithSegment(moduleSegment), logger.Named("modules")) s.deckhouseService = NewDeckhouseService(base, logger.Named("deckhouse")) s.security = NewSecurityServices(securityServiceName, base.WithSegment(securitySegment), logger.Named("security")) - // services that are not scoped by edition + // Edition-independent services (built from c, without edition segment): + // plugins -> /plugins + // installer -> /installer s.pluginService = NewPluginService(c.WithSegment(pluginSegment), logger.Named("plugins")) s.installer = NewInstallerServices(installerServiceName, c.WithSegment("installer"), logger.Named("installer")) diff --git a/pkg/registry/service/service_root_test.go b/pkg/registry/service/service_root_test.go new file mode 100644 index 00000000..113efd85 --- /dev/null +++ b/pkg/registry/service/service_root_test.go @@ -0,0 +1,67 @@ +/* +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 service_test + +import ( + "log/slog" + "testing" + + "github.com/stretchr/testify/require" + + dkplog "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/deckhouse-cli/pkg" + registryclient "github.com/deckhouse/deckhouse-cli/pkg/registry/client" + registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service" +) + +func TestNewService_RootScopes(t *testing.T) { + tests := []struct { + name string + registryRoot string + edition pkg.Edition + wantServiceRoot string + wantDeckhouseRoot string + }{ + { + name: "edition layout keeps root split", + registryRoot: "registry.example.com/deckhouse", + edition: pkg.FEEdition, + wantServiceRoot: "registry.example.com/deckhouse", + wantDeckhouseRoot: "registry.example.com/deckhouse/fe", + }, + { + name: "flat layout keeps same root for both services", + registryRoot: "registry.example.com/deckhouse", + edition: pkg.NoEdition, + wantServiceRoot: "registry.example.com/deckhouse", + wantDeckhouseRoot: "registry.example.com/deckhouse", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn)) + client := registryclient.NewFromOptions(tc.registryRoot) + + svc := registryservice.NewService(client, tc.edition, logger) + + require.Equal(t, tc.wantServiceRoot, svc.GetRoot()) + require.Equal(t, tc.wantDeckhouseRoot, svc.DeckhouseService().GetRoot()) + }) + } +}