Skip to content
Draft
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
11 changes: 8 additions & 3 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -259,12 +264,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
1 change: 1 addition & 0 deletions cmd/plugins/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ func (pc *PluginsCommand) InitPluginServices() {

registryService := service.NewService(
pc.pluginRegistryClient,
"",
pc.logger.Named("registry-service"),
)

Expand Down
1 change: 1 addition & 0 deletions internal/mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ const (
MirrorTypeSecurityTrivyBDUSegment
MirrorTypeSecurityTrivyJavaDBSegment
MirrorTypeSecurityTrivyChecksSegment
// MirrorTypeInstaller
)
7 changes: 7 additions & 0 deletions internal/mirror/cmd/pull/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ var (
NoPlatform bool
NoSecurityDB bool
NoModules bool
NoInstaller bool
OnlyExtraImages bool
)

Expand Down Expand Up @@ -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",
Expand Down
49 changes: 47 additions & 2 deletions internal/mirror/cmd/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import (
"os/signal"
"path"
"path/filepath"
"slices"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -283,13 +287,14 @@ 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{
SkipPlatform: pullflags.NoPlatform,
SkipSecurity: pullflags.NoSecurityDB,
SkipModules: pullflags.NoModules,
SkipInstaller: pullflags.NoInstaller,
OnlyExtraImages: pullflags.OnlyExtraImages,
IgnoreSuspend: pullflags.IgnoreSuspend,
ModuleFilter: filter,
Expand Down Expand Up @@ -342,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 {
Expand Down
199 changes: 199 additions & 0 deletions internal/mirror/installer/installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
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"
"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"
)

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
// 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
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, 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{}
}

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 := svc.findTagsToMirror(ctx)

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 {
targetTag := defaultTargetTag

if svc.options.TargetTag != "" {
targetTag = svc.options.TargetTag
}

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,
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)
}

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
}
Loading