From 9e4b8706e5215c1bd66ae44a9669c7a935ad1e35 Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Thu, 19 Feb 2026 17:27:09 +0300 Subject: [PATCH 1/7] pull prototype Signed-off-by: Smyslov Maxim --- internal/mirror.go | 1 + internal/mirror/cmd/pull/flags/flags.go | 7 + internal/mirror/cmd/pull/pull.go | 1 + internal/mirror/installer/installer.go | 165 ++++++++++++++++++++++ internal/mirror/installer/layout.go | 78 ++++++++++ internal/mirror/pull.go | 24 +++- pkg/registry/service/installer_service.go | 32 +++++ pkg/registry/service/service.go | 6 + 8 files changed, 311 insertions(+), 3 deletions(-) create mode 100644 internal/mirror/installer/installer.go create mode 100644 internal/mirror/installer/layout.go create mode 100644 pkg/registry/service/installer_service.go diff --git a/internal/mirror.go b/internal/mirror.go index 3e87f9e8..fef72920 100644 --- a/internal/mirror.go +++ b/internal/mirror.go @@ -30,4 +30,5 @@ const ( MirrorTypeSecurityTrivyBDUSegment MirrorTypeSecurityTrivyJavaDBSegment MirrorTypeSecurityTrivyChecksSegment + // MirrorTypeInstaller ) diff --git a/internal/mirror/cmd/pull/flags/flags.go b/internal/mirror/cmd/pull/flags/flags.go index c5487349..6929577a 100644 --- a/internal/mirror/cmd/pull/flags/flags.go +++ b/internal/mirror/cmd/pull/flags/flags.go @@ -62,6 +62,7 @@ var ( NoPlatform bool NoSecurityDB bool NoModules bool + NoInstaller bool OnlyExtraImages bool ) @@ -186,6 +187,12 @@ module-name@=v1.3.0+stable → exact tag match: include only v1.3.0 and and publ false, "Do not pull Deckhouse modules into bundle.", ) + flagSet.BoolVar( + &NoInstaller, + "no-installer", + false, + "Do not pull Deckhouse installer into bundle.", + ) flagSet.BoolVar( &OnlyExtraImages, "only-extra-images", diff --git a/internal/mirror/cmd/pull/pull.go b/internal/mirror/cmd/pull/pull.go index 57b6ec97..7b406ba0 100644 --- a/internal/mirror/cmd/pull/pull.go +++ b/internal/mirror/cmd/pull/pull.go @@ -290,6 +290,7 @@ func (p *Puller) Execute(ctx context.Context) error { SkipPlatform: pullflags.NoPlatform, SkipSecurity: pullflags.NoSecurityDB, SkipModules: pullflags.NoModules, + SkipInstaller: pullflags.NoInstaller, OnlyExtraImages: pullflags.OnlyExtraImages, IgnoreSuspend: pullflags.IgnoreSuspend, ModuleFilter: filter, diff --git a/internal/mirror/installer/installer.go b/internal/mirror/installer/installer.go new file mode 100644 index 00000000..c8c28f6a --- /dev/null +++ b/internal/mirror/installer/installer.go @@ -0,0 +1,165 @@ +/* +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 installer + +import ( + "context" + "fmt" + "log/slog" + "path/filepath" + "time" + + dkplog "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/deckhouse-cli/internal/mirror/puller" + "github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log" + registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service" +) + +const defaultTargetTag = "latest" + +type Options struct { + // TargetTag specifies a specific tag to mirror instead of determining versions automatically + // it can be: + // semver f.e. vX.Y.Z + // channel f.e. alpha/beta/stable + // any other tag + TargetTag string +} +type Service struct { + // registryService handles Deckhouse installer registry operations + registryService *registryservice.Service + // layout manages the OCI image layouts for installer components + layout *ImageLayouts + // downloadList manages the list of images to be downloaded + downloadList *ImageDownloadList + // pullerService handles the pulling of images + pullerService *puller.PullerService + + // options contains service configuration + options *Options + + // logger is for internal debug logging + logger *dkplog.Logger + // userLogger is for user-facing informational messages + userLogger *log.SLogger +} + +func NewService( + registryService *registryservice.Service, + workingDir string, + options *Options, + logger *dkplog.Logger, + userLogger *log.SLogger, +) *Service { + userLogger.Infof("Creating OCI Image Layouts for Installer") + + // workingDir is the root where we create layouts + // Layouts will be created at workingDir/installer/ + layout := NewImageLayouts(filepath.Join(workingDir, "installer")) + + if options == nil { + options = &Options{} + } + + return &Service{ + registryService: registryService, + layout: layout, + downloadList: NewImageDownloadList(registryService.GetRoot()), + pullerService: puller.NewPullerService(logger, userLogger), + options: options, + logger: logger, + userLogger: userLogger, + } +} + +// PullInstaller pulls the installer image +// It validates access to the registry and pulls the image +func (svc *Service) PullInstaller(ctx context.Context) error { + // logger := svc.userLogger + + err := svc.validateInstallerAccess(ctx) + if err != nil { + return fmt.Errorf("validate installer access: %w", err) + } + + tagsToMirror, err := svc.findTagsToMirror(ctx) + if err != nil { + return fmt.Errorf("find tags to mirror: %w", err) + } + + svc.downloadList.FillInstallerImages(tagsToMirror) + + err = svc.pullInstaller(ctx) + if err != nil { + return fmt.Errorf("pull installer: %w", err) + } + + return nil +} + +func (svc *Service) validateInstallerAccess(ctx context.Context) error { + targetTag := defaultTargetTag + + if svc.options.TargetTag != "" { + targetTag = svc.options.TargetTag + } + + svc.logger.Debug("Validating access to the installer registry", slog.String("tag", targetTag)) + + ctx, cancel := context.WithTimeout(ctx, 15*time.Second) + defer cancel() + + err := svc.registryService.InstallerService().CheckImageExists(ctx, targetTag) + if err != nil { + return fmt.Errorf("failed to check installer tag %q exists in registry: %w", targetTag, err) + } + + return nil +} + +func (svc *Service) findTagsToMirror(_ context.Context) ([]string, error) { + targetTag := defaultTargetTag + + if svc.options.TargetTag != "" { + targetTag = svc.options.TargetTag + } + + return []string{targetTag}, nil +} + +func (svc *Service) pullInstaller(ctx context.Context) error { + logger := svc.userLogger + + err := logger.Process("Pull installer", func() error { + + config := puller.PullConfig{ + Name: "installer", + ImageSet: svc.downloadList.Installer, + Layout: svc.layout.image, + AllowMissingTags: svc.options.TargetTag == "", + GetterService: svc.registryService.InstallerService(), + } + + return svc.pullerService.PullImages(ctx, config) + }) + if err != nil { + return fmt.Errorf("pull installer: %w", err) + } + + return nil +} diff --git a/internal/mirror/installer/layout.go b/internal/mirror/installer/layout.go new file mode 100644 index 00000000..9865f343 --- /dev/null +++ b/internal/mirror/installer/layout.go @@ -0,0 +1,78 @@ +/* +Copyright 2025 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 installer + +import ( + "github.com/deckhouse/deckhouse-cli/internal/mirror/puller" + "github.com/deckhouse/deckhouse-cli/pkg/registry/image" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" +) + +// import ( +// "fmt" +// "path" +// "path/filepath" + +// v1 "github.com/google/go-containerregistry/pkg/v1" +// "github.com/google/go-containerregistry/pkg/v1/layout" + +// "github.com/deckhouse/deckhouse-cli/internal" +// "github.com/deckhouse/deckhouse-cli/internal/mirror/puller" +// regimage "github.com/deckhouse/deckhouse-cli/pkg/registry/image" +// ) + +type ImageDownloadList struct { + rootURL string + + Installer map[string]*puller.ImageMeta +} + +func NewImageDownloadList(rootURL string) *ImageDownloadList { + return &ImageDownloadList{ + rootURL: rootURL, + + Installer: make(map[string]*puller.ImageMeta), + } +} + +func (l *ImageDownloadList) FillInstallerImages(tagsToMirror []string) { + for _, tag := range tagsToMirror { + l.Installer[l.rootURL+":"+tag] = nil + } +} + +type ImageLayouts struct { + platform v1.Platform + workingDir string + image *image.ImageLayout +} + +func NewImageLayouts(rootFolder string) *ImageLayouts { + l := &ImageLayouts{ + workingDir: rootFolder, + platform: v1.Platform{Architecture: "amd64", OS: "linux"}, + image: new(image.ImageLayout), + } + + return l +} + +// AsList returns a list of layout.Path's in it. Undefined path's are not included in the list. +func (l *ImageLayouts) AsList() []layout.Path { + return []layout.Path{l.image.Path()} +} diff --git a/internal/mirror/pull.go b/internal/mirror/pull.go index 0c9ab239..6c648b1f 100644 --- a/internal/mirror/pull.go +++ b/internal/mirror/pull.go @@ -22,6 +22,7 @@ import ( dkplog "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/deckhouse-cli/internal/mirror/installer" "github.com/deckhouse/deckhouse-cli/internal/mirror/modules" "github.com/deckhouse/deckhouse-cli/internal/mirror/platform" "github.com/deckhouse/deckhouse-cli/internal/mirror/security" @@ -38,6 +39,8 @@ type PullServiceOptions struct { SkipSecurity bool // SkipModules skips pulling module images SkipModules bool + // SkipInstaller skips pulling installer images + SkipInstaller bool // OnlyExtraImages pulls only extra images for modules (without main module images) OnlyExtraImages bool // IgnoreSuspend allows mirroring even if release channels are suspended @@ -53,9 +56,10 @@ type PullServiceOptions struct { type PullService struct { registryService *registryservice.Service - platformService *platform.Service - securityService *security.Service - modulesService *modules.Service + platformService *platform.Service + securityService *security.Service + modulesService *modules.Service + installerService *installer.Service options *PullServiceOptions @@ -117,6 +121,13 @@ func NewPullService( logger, userLogger, ), + installerService: installer.NewService( + registryService, + tmpDir, + nil, + logger, + userLogger, + ), options: options, @@ -150,5 +161,12 @@ func (svc *PullService) Pull(ctx context.Context) error { } } + if !svc.options.SkipInstaller { + err := svc.installerService.PullInstaller(ctx) + if err != nil { + return fmt.Errorf("pull installer: %w", err) + } + } + return nil } diff --git a/pkg/registry/service/installer_service.go b/pkg/registry/service/installer_service.go new file mode 100644 index 00000000..77fc1fff --- /dev/null +++ b/pkg/registry/service/installer_service.go @@ -0,0 +1,32 @@ +package service + +import ( + "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/deckhouse/pkg/registry" +) + +type InstallerServices struct { + *BasicService + + client registry.Client + + logger *log.Logger +} + +func NewInstallerServices(name string, client registry.Client, logger *log.Logger) *InstallerServices { + return &InstallerServices{ + BasicService: NewBasicService(name, client, logger), + client: client, + logger: logger, + } +} + +// func (s *InstallerServices) Installer(imageName string) *BasicService { +// if service, exists := s.securityServices[imageName]; exists { +// return service +// } + +// s.securityServices[imageName] = NewBasicService(s.name+" "+imageName, s.client.WithSegment(imageName), s.logger) + +// return s.securityServices[imageName] +// } diff --git a/pkg/registry/service/service.go b/pkg/registry/service/service.go index e42ee88c..20dbe1b2 100644 --- a/pkg/registry/service/service.go +++ b/pkg/registry/service/service.go @@ -37,6 +37,7 @@ type Service struct { pluginService *PluginService deckhouseService *DeckhouseService security *SecurityServices + installer *InstallerServices logger *log.Logger } @@ -52,6 +53,7 @@ func NewService(client registry.Client, logger *log.Logger) *Service { s.pluginService = NewPluginService(client.WithSegment(pluginSegment), logger.Named("plugins")) s.deckhouseService = NewDeckhouseService(client, logger.Named("deckhouse")) s.security = NewSecurityServices(securityServiceName, client.WithSegment(securitySegment), logger.Named("security")) + s.installer = NewInstallerServices(installerServiceName, client.WithSegment(installerSegment), logger.Named("installer")) return s } @@ -79,3 +81,7 @@ func (s *Service) DeckhouseService() *DeckhouseService { func (s *Service) Security() *SecurityServices { return s.security } + +func (s *Service) InstallerService() *InstallerServices { + return s.installer +} From 43f13d11a36914f7a79e8ff5d6e734838f880cb2 Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Thu, 19 Feb 2026 18:30:01 +0300 Subject: [PATCH 2/7] edition Signed-off-by: Smyslov Maxim --- cmd/plugins/init.go | 1 + internal/mirror/cmd/pull/pull.go | 48 +++++++++++++++++++++++-- pkg/libmirror/operations/params/pull.go | 1 + pkg/registry/service/service.go | 12 +++---- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/cmd/plugins/init.go b/cmd/plugins/init.go index 459ae781..79d50928 100644 --- a/cmd/plugins/init.go +++ b/cmd/plugins/init.go @@ -71,6 +71,7 @@ func (pc *PluginsCommand) InitPluginServices() { registryService := service.NewService( pc.pluginRegistryClient, + "", pc.logger.Named("registry-service"), ) diff --git a/internal/mirror/cmd/pull/pull.go b/internal/mirror/cmd/pull/pull.go index 7b406ba0..f53cc28b 100644 --- a/internal/mirror/cmd/pull/pull.go +++ b/internal/mirror/cmd/pull/pull.go @@ -26,6 +26,8 @@ import ( "os/signal" "path" "path/filepath" + "slices" + "strings" "syscall" "time" @@ -177,6 +179,7 @@ func buildPullParams(logger params.Logger) *params.PullParams { SkipPlatform: pullflags.NoPlatform, SkipSecurityDatabases: pullflags.NoSecurityDB, SkipModules: pullflags.NoModules, + SkipInstaller: pullflags.NoInstaller, OnlyExtraImages: pullflags.OnlyExtraImages, IgnoreSuspend: pullflags.IgnoreSuspend, DeckhouseTag: pullflags.DeckhouseTag, @@ -265,7 +268,8 @@ func (p *Puller) Execute(ctx context.Context) error { } var c registry.Client - c = regclient.NewClientWithOptions(p.params.DeckhouseRegistryRepo, clientOpts) + repo, edition := cutEditionFromRegistryRepo(p.params.DeckhouseRegistryRepo) + c = regclient.NewClientWithOptions(repo, clientOpts) if os.Getenv("STUB_REGISTRY_CLIENT") == "true" { c = stub.NewRegistryClientStub() @@ -283,7 +287,7 @@ func (p *Puller) Execute(ctx context.Context) error { } svc := mirror.NewPullService( - registryservice.NewService(c, logger), + registryservice.NewService(c, edition, logger), pullflags.TempDir, pullflags.DeckhouseTag, &mirror.PullServiceOptions{ @@ -343,6 +347,46 @@ func (p *Puller) cleanupWorkingDirectory() error { return nil } +var enumEditions = []string{ + "ee", + "fe", + "se", + "be", + "se-plus", + "ce", +} + +// cutEditionFromRegistryRepo cuts the edition from the registry repository +// returns the registry repository without the edition and the edition +// this is needed because of the different paths for the installer images in the registry +// example: +// registry.deckhouse.ru/deckhouse/ee/ -> registry.deckhouse.ru/deckhouse, ee +// myregistry.ru/deckhouse/ -> myregistry.ru/deckhouse, "" +func cutEditionFromRegistryRepo(registryRepo string) (string, string) { + // strip last slash + registry := strings.TrimSuffix(registryRepo, "/") + + // split by / + parts := strings.Split(registry, "/") + + // get last element + lastPart := parts[len(parts)-1] + + // if not edition, return registry + /installer + if !slices.Contains(enumEditions, strings.ToLower(lastPart)) { + return registry, "" + } + edition := lastPart + + // cut edition + newparts := parts[:len(parts)-1] + + // join back together + newregistry := strings.Join(newparts, "/") + + return newregistry, edition +} + // pullPlatform pulls the Deckhouse platform components func (p *Puller) pullPlatform() error { if p.params.SkipPlatform { diff --git a/pkg/libmirror/operations/params/pull.go b/pkg/libmirror/operations/params/pull.go index c674840b..8b430513 100644 --- a/pkg/libmirror/operations/params/pull.go +++ b/pkg/libmirror/operations/params/pull.go @@ -28,6 +28,7 @@ type PullParams struct { SkipPlatform bool // --no-platform SkipSecurityDatabases bool // --no-security-db SkipModules bool // --no-modules + SkipInstaller bool // --no-installer OnlyExtraImages bool // --only-extra-images IgnoreSuspend bool // --ignore-suspend BundleChunkSize int64 // Plain bytes diff --git a/pkg/registry/service/service.go b/pkg/registry/service/service.go index 20dbe1b2..5cbb3f7f 100644 --- a/pkg/registry/service/service.go +++ b/pkg/registry/service/service.go @@ -43,17 +43,17 @@ type Service struct { } // NewService creates a new registry service with the given client and logger -func NewService(client registry.Client, logger *log.Logger) *Service { +func NewService(client registry.Client, edition string, logger *log.Logger) *Service { s := &Service{ client: client, logger: logger, } - s.modulesService = NewModulesService(client.WithSegment(moduleSegment), logger.Named("modules")) - s.pluginService = NewPluginService(client.WithSegment(pluginSegment), logger.Named("plugins")) - s.deckhouseService = NewDeckhouseService(client, logger.Named("deckhouse")) - s.security = NewSecurityServices(securityServiceName, client.WithSegment(securitySegment), logger.Named("security")) - s.installer = NewInstallerServices(installerServiceName, client.WithSegment(installerSegment), logger.Named("installer")) + s.modulesService = NewModulesService(client.WithSegment(edition, moduleSegment), logger.Named("modules")) + s.pluginService = NewPluginService(client.WithSegment(edition, pluginSegment), logger.Named("plugins")) + s.deckhouseService = NewDeckhouseService(client.WithSegment(edition), logger.Named("deckhouse")) + s.security = NewSecurityServices(securityServiceName, client.WithSegment(edition, securitySegment), logger.Named("security")) + s.installer = NewInstallerServices(installerServiceName, client.WithSegment("installer"), logger.Named("installer")) return s } From 46d11991a898c39d7aec8dfcb3caa5f9b18d87d4 Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Fri, 20 Feb 2026 11:48:16 +0300 Subject: [PATCH 3/7] fix pull Signed-off-by: Smyslov Maxim --- internal/mirror/installer/installer.go | 16 ++++++++-------- internal/mirror/installer/layout.go | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/internal/mirror/installer/installer.go b/internal/mirror/installer/installer.go index c8c28f6a..406d4ef7 100644 --- a/internal/mirror/installer/installer.go +++ b/internal/mirror/installer/installer.go @@ -70,7 +70,11 @@ func NewService( // workingDir is the root where we create layouts // Layouts will be created at workingDir/installer/ - layout := NewImageLayouts(filepath.Join(workingDir, "installer")) + layout, err := NewImageLayouts(filepath.Join(workingDir, "installer")) + if err != nil { + //TODO: handle error + userLogger.Warnf("Create OCI Image Layouts: %v", err) + } if options == nil { options = &Options{} @@ -97,10 +101,7 @@ func (svc *Service) PullInstaller(ctx context.Context) error { return fmt.Errorf("validate installer access: %w", err) } - tagsToMirror, err := svc.findTagsToMirror(ctx) - if err != nil { - return fmt.Errorf("find tags to mirror: %w", err) - } + tagsToMirror := svc.findTagsToMirror(ctx) svc.downloadList.FillInstallerImages(tagsToMirror) @@ -132,21 +133,20 @@ func (svc *Service) validateInstallerAccess(ctx context.Context) error { return nil } -func (svc *Service) findTagsToMirror(_ context.Context) ([]string, error) { +func (svc *Service) findTagsToMirror(_ context.Context) []string { targetTag := defaultTargetTag if svc.options.TargetTag != "" { targetTag = svc.options.TargetTag } - return []string{targetTag}, nil + return []string{targetTag} } func (svc *Service) pullInstaller(ctx context.Context) error { logger := svc.userLogger err := logger.Process("Pull installer", func() error { - config := puller.PullConfig{ Name: "installer", ImageSet: svc.downloadList.Installer, diff --git a/internal/mirror/installer/layout.go b/internal/mirror/installer/layout.go index 9865f343..c007ff19 100644 --- a/internal/mirror/installer/layout.go +++ b/internal/mirror/installer/layout.go @@ -17,6 +17,9 @@ limitations under the License. package installer import ( + "fmt" + "path/filepath" + "github.com/deckhouse/deckhouse-cli/internal/mirror/puller" "github.com/deckhouse/deckhouse-cli/pkg/registry/image" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -62,14 +65,20 @@ type ImageLayouts struct { image *image.ImageLayout } -func NewImageLayouts(rootFolder string) *ImageLayouts { +func NewImageLayouts(rootFolder string) (*ImageLayouts, error) { + layoutPath := filepath.Join(rootFolder, "installer") + image, err := image.NewImageLayout(layoutPath) + if err != nil { + return nil, fmt.Errorf("failed to create image layout: %w", err) + } + l := &ImageLayouts{ workingDir: rootFolder, platform: v1.Platform{Architecture: "amd64", OS: "linux"}, - image: new(image.ImageLayout), + image: image, } - return l + return l, nil } // AsList returns a list of layout.Path's in it. Undefined path's are not included in the list. From fc6040ab0ef5b73a39478b99e0e9acb4d82deade Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Fri, 20 Feb 2026 13:47:56 +0300 Subject: [PATCH 4/7] test binary artifact Signed-off-by: Smyslov Maxim --- .github/workflows/release.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ae870b06..c679bf65 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -259,12 +259,12 @@ jobs: git config --global --add safe.directory '*' - name: Build and package - run: task build-and-package + run: task build:dist:all - name: Upload build artifacts uses: actions/upload-artifact@v4 with: - name: deckhouse-cli-pr-build-${{ github.run_id }}.tar.gz - path: dist/**/*.tar.gz + name: deckhouse-cli-pr-build-${{ github.run_id }} + path: dist/**/* retention-days: 1 compression-level: 9 From 4577839713fa592c9f6138c28b67fc1a86e06033 Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Fri, 20 Feb 2026 13:49:23 +0300 Subject: [PATCH 5/7] fix lint Signed-off-by: Smyslov Maxim --- internal/mirror/installer/layout.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/mirror/installer/layout.go b/internal/mirror/installer/layout.go index c007ff19..5cbd351a 100644 --- a/internal/mirror/installer/layout.go +++ b/internal/mirror/installer/layout.go @@ -20,10 +20,11 @@ import ( "fmt" "path/filepath" - "github.com/deckhouse/deckhouse-cli/internal/mirror/puller" - "github.com/deckhouse/deckhouse-cli/pkg/registry/image" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/layout" + + "github.com/deckhouse/deckhouse-cli/internal/mirror/puller" + "github.com/deckhouse/deckhouse-cli/pkg/registry/image" ) // import ( From cc460408644df55d93d0caf4056e6b6ace316f5c Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Fri, 20 Feb 2026 13:54:05 +0300 Subject: [PATCH 6/7] add cancel-in-progress to ci Signed-off-by: Smyslov Maxim --- .github/workflows/release.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c679bf65..e48f48fe 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,6 +16,11 @@ env: permissions: contents: write +# Cancel in-progress jobs for the same PR (pull_request_target event) or for the same branch (push event). +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + jobs: release: if: startsWith(github.ref, 'refs/tags/v') From 4dce10a8000a718849d0e6c3d3bce8abc5752509 Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Fri, 20 Feb 2026 14:43:38 +0300 Subject: [PATCH 7/7] fix pull not packs Signed-off-by: Smyslov Maxim --- internal/mirror/installer/installer.go | 34 ++++++++++++++++++++++++++ internal/mirror/installer/layout.go | 19 +++----------- internal/mirror/pull.go | 6 ++++- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/internal/mirror/installer/installer.go b/internal/mirror/installer/installer.go index 406d4ef7..18229f1b 100644 --- a/internal/mirror/installer/installer.go +++ b/internal/mirror/installer/installer.go @@ -19,13 +19,17 @@ package installer import ( "context" "fmt" + "io" "log/slog" + "os" "path/filepath" "time" dkplog "github.com/deckhouse/deckhouse/pkg/log" + "github.com/deckhouse/deckhouse-cli/internal/mirror/chunked" "github.com/deckhouse/deckhouse-cli/internal/mirror/puller" + "github.com/deckhouse/deckhouse-cli/pkg/libmirror/bundle" "github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log" registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service" ) @@ -39,6 +43,10 @@ type Options struct { // channel f.e. alpha/beta/stable // any other tag TargetTag string + // BundleDir is the directory to store the bundle + BundleDir string + // BundleChunkSize is the max size of bundle chunks in bytes (0 = no chunking) + BundleChunkSize int64 } type Service struct { // registryService handles Deckhouse installer registry operations @@ -161,5 +169,31 @@ func (svc *Service) pullInstaller(ctx context.Context) error { return fmt.Errorf("pull installer: %w", err) } + if err := logger.Process("Pack Deckhouse images into platform.tar", func() error { + bundleChunkSize := svc.options.BundleChunkSize + bundleDir := svc.options.BundleDir + + var installer io.Writer = chunked.NewChunkedFileWriter( + bundleChunkSize, + bundleDir, + "installer.tar", + ) + + if bundleChunkSize == 0 { + installer, err = os.Create(filepath.Join(bundleDir, "installer.tar")) + if err != nil { + return fmt.Errorf("create installer.tar: %w", err) + } + } + + if err := bundle.Pack(context.Background(), svc.layout.workingDir, installer); err != nil { + return fmt.Errorf("pack installer.tar: %w", err) + } + + return nil + }); err != nil { + return err + } + return nil } diff --git a/internal/mirror/installer/layout.go b/internal/mirror/installer/layout.go index 5cbd351a..2e8be76b 100644 --- a/internal/mirror/installer/layout.go +++ b/internal/mirror/installer/layout.go @@ -24,22 +24,9 @@ import ( "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/deckhouse/deckhouse-cli/internal/mirror/puller" - "github.com/deckhouse/deckhouse-cli/pkg/registry/image" + regimage "github.com/deckhouse/deckhouse-cli/pkg/registry/image" ) -// import ( -// "fmt" -// "path" -// "path/filepath" - -// v1 "github.com/google/go-containerregistry/pkg/v1" -// "github.com/google/go-containerregistry/pkg/v1/layout" - -// "github.com/deckhouse/deckhouse-cli/internal" -// "github.com/deckhouse/deckhouse-cli/internal/mirror/puller" -// regimage "github.com/deckhouse/deckhouse-cli/pkg/registry/image" -// ) - type ImageDownloadList struct { rootURL string @@ -63,12 +50,12 @@ func (l *ImageDownloadList) FillInstallerImages(tagsToMirror []string) { type ImageLayouts struct { platform v1.Platform workingDir string - image *image.ImageLayout + image *regimage.ImageLayout } func NewImageLayouts(rootFolder string) (*ImageLayouts, error) { layoutPath := filepath.Join(rootFolder, "installer") - image, err := image.NewImageLayout(layoutPath) + image, err := regimage.NewImageLayout(layoutPath) if err != nil { return nil, fmt.Errorf("failed to create image layout: %w", err) } diff --git a/internal/mirror/pull.go b/internal/mirror/pull.go index 6c648b1f..07c187ce 100644 --- a/internal/mirror/pull.go +++ b/internal/mirror/pull.go @@ -124,7 +124,11 @@ func NewPullService( installerService: installer.NewService( registryService, tmpDir, - nil, + &installer.Options{ + TargetTag: targetTag, + BundleDir: options.BundleDir, + BundleChunkSize: options.BundleChunkSize, + }, logger, userLogger, ),