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
41 changes: 41 additions & 0 deletions internal/cli/action/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright © 2025-2026 SUSE LLC
SPDX-License-Identifier: Apache-2.0

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 action

import (
"fmt"
"path/filepath"
)

func resolveCacheDir(cache bool, cacheDir string) (string, error) {
if !cache {
return "", nil
}

if cacheDir == "" {
return "", fmt.Errorf("cache directory not set")
}

abs, err := filepath.Abs(cacheDir)
if err != nil {
return "", fmt.Errorf("resolving cache directory %q: %w", cacheDir, err)
}

return abs, nil
}
24 changes: 17 additions & 7 deletions internal/cli/action/customize.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ func Customize(ctx context.Context, cmd *cli.Command) error {
ctxCancel, cancelFunc := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT)
defer cancelFunc()

customizeRunner, err := setupCustomizeRunner(ctxCancel, system, args, output)
cacheDir, err := resolveCacheDir(args.Cache, args.CacheDir)
if err != nil {
logger.Error("Resolving cache directory failed")
return err
}

customizeRunner, err := setupCustomizeRunner(ctxCancel, system, args, output, cacheDir)
if err != nil {
logger.Error("Setting up customization runner failed")
return err
Expand Down Expand Up @@ -115,20 +121,21 @@ func setupCustomizeRunner(
s *sys.System,
args *cmdpkg.CustomizeFlags,
output config.Output,
cacheDir string,
) (*customize.Runner, error) {
extr, err := setupFileExtractor(ctx, s, output, args.Local)
extr, err := setupFileExtractor(ctx, s, output, cacheDir, args.Offline)
if err != nil {
return nil, fmt.Errorf("setting up file extractor: %w", err)
}

return &customize.Runner{
System: s,
ConfigManager: setupConfigManager(s, args.ConfigDir, output, args.Local),
ConfigManager: setupConfigManager(s, args.ConfigDir, output, args.Airgap, cacheDir, args.Offline),
FileExtractor: extr,
}, nil
}

func setupConfigManager(s *sys.System, configDir string, output config.Output, local bool) *config.Manager {
func setupConfigManager(s *sys.System, configDir string, output config.Output, airgap bool, cacheDir string, offline bool) *config.Manager {
valuesResolver := &helm.ValuesResolver{
FS: s.FS(),
ValuesDir: v0.Dir(configDir).HelmValuesDir(),
Expand All @@ -138,11 +145,13 @@ func setupConfigManager(s *sys.System, configDir string, output config.Output, l
s,
config.NewHelm(s.FS(), valuesResolver, s.Logger(), output.OverlaysDir()),
config.WithDownloadFunc(http.DownloadFile),
config.WithLocal(local),
config.WithAirgap(airgap),
config.WithCacheDir(cacheDir),
config.WithOffline(offline),
)
}

func setupFileExtractor(ctx context.Context, s *sys.System, outDir config.Output, local bool) (extr *extractor.OCIFileExtractor, err error) {
func setupFileExtractor(ctx context.Context, s *sys.System, outDir config.Output, cacheDir string, offline bool) (extr *extractor.OCIFileExtractor, err error) {
const isoSearchGlob = "/iso/*default-iso*.iso"

if err := vfs.MkdirAll(s.FS(), outDir.ISOStoreDir(), vfs.DirPerm); err != nil {
Expand All @@ -154,7 +163,8 @@ func setupFileExtractor(ctx context.Context, s *sys.System, outDir config.Output
extractor.WithStore(outDir.ISOStoreDir()),
extractor.WithFS(s.FS()),
extractor.WithContext(ctx),
extractor.WithLocal(local),
extractor.WithCacheDir(cacheDir),
extractor.WithOffline(offline),
)
}

Expand Down
40 changes: 40 additions & 0 deletions internal/cli/cmd/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright © 2025-2026 SUSE LLC
SPDX-License-Identifier: Apache-2.0

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 cmd

import (
"fmt"
"path/filepath"
)

const defaultCacheDir = "/elemental-cache"

func ValidateCacheFlags(cache bool, cacheDir string, offline bool) error {
if cache {
return nil
}

if offline {
return fmt.Errorf("--offline requires --cache")
}
if cacheDir != "" && filepath.Clean(cacheDir) != defaultCacheDir {
return fmt.Errorf("--cache-dir requires --cache")
}

return nil
}
18 changes: 17 additions & 1 deletion internal/cli/cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,23 @@ package cmd
const (
// --local flag name and description
localFlg = "local"
localDesc = "Load OCI images from the local container storage instead of a remote registry"
localDesc = "Load OCI images from local container storage instead of a remote registry"

// --airgap flag name and description
airgapFlg = "airgap"
airgapDesc = "Embed container images for air-gapped Kubernetes deployments"

// --cache flag name and description
cacheFlg = "cache"
cacheDesc = "Use the OCI image cache while preparing build artifacts"

// --cache-dir flag name and description
cacheDirFlg = "cache-dir"
cacheDirDesc = "Path to the OCI image cache directory"

// --offline flag name and description
offlineFlg = "offline"
offlineDesc = "Use only cached OCI images without pulling from remote registries"

// --verify flag name and description
verifyFlg = "verify"
Expand Down
33 changes: 29 additions & 4 deletions internal/cli/cmd/customize.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ type CustomizeFlags struct {
Mode string
Platform string
MediaType string
Local bool
Airgap bool
Cache bool
CacheDir string
Offline bool
}

var CustomizeArgs CustomizeFlags
Expand All @@ -49,6 +52,10 @@ func NewCustomizeCommand(appName string, action func(context.Context, *cli.Comma
return ctx, cli.Exit("Error: Unsupported --mode option.", 1)
}

if err := ValidateCacheFlags(CustomizeArgs.Cache, CustomizeArgs.CacheDir, CustomizeArgs.Offline); err != nil {
return ctx, cli.Exit(fmt.Sprintf("Error: %v.", err), 1)
}

return ctx, nil
},
Action: action,
Expand Down Expand Up @@ -86,9 +93,27 @@ func NewCustomizeCommand(appName string, action func(context.Context, *cli.Comma
Value: fmt.Sprintf("linux/%s", runtime.GOARCH),
},
&cli.BoolFlag{
Name: localFlg,
Usage: localDesc,
Destination: &CustomizeArgs.Local,
Name: airgapFlg,
Usage: airgapDesc,
Destination: &CustomizeArgs.Airgap,
Value: true,
},
&cli.BoolFlag{
Name: cacheFlg,
Usage: cacheDesc,
Destination: &CustomizeArgs.Cache,
Value: true,
},
&cli.StringFlag{
Name: cacheDirFlg,
Usage: cacheDirDesc,
Destination: &CustomizeArgs.CacheDir,
Value: defaultCacheDir,
},
&cli.BoolFlag{
Name: offlineFlg,
Usage: offlineDesc,
Destination: &CustomizeArgs.Offline,
},
},
}
Expand Down
30 changes: 22 additions & 8 deletions internal/config/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@ type releaseManifestResolver interface {
}

type Manager struct {
system *sys.System
local bool
system *sys.System
airgap bool
cacheDir string
offline bool

rmResolver releaseManifestResolver
downloadFile downloadFunc
Expand Down Expand Up @@ -73,9 +75,21 @@ func WithUnpackFunc(u unpackFunc) Opts {
}
}

func WithLocal(local bool) Opts {
func WithAirgap(airgap bool) Opts {
return func(m *Manager) {
m.local = local
m.airgap = airgap
}
}

func WithCacheDir(cacheDir string) Opts {
return func(m *Manager) {
m.cacheDir = cacheDir
}
}

func WithOffline(offline bool) Opts {
return func(m *Manager) {
m.offline = offline
}
}

Expand All @@ -95,7 +109,7 @@ func NewManager(sys *sys.System, helm helmConfigurator, opts ...Opts) *Manager {

if m.unpackImage == nil {
m.unpackImage = func(ctx context.Context, imageRef, destDir string) error {
unpacker := unpack.NewOCIUnpacker(sys, imageRef, unpack.WithLocalOCI(m.local))
unpacker := unpack.NewOCIUnpacker(sys, imageRef, unpack.WithCacheDirOCI(m.cacheDir), unpack.WithOfflineOCI(m.offline))
_, err := unpacker.Unpack(ctx, destDir)
return err
}
Expand All @@ -108,7 +122,7 @@ func NewManager(sys *sys.System, helm helmConfigurator, opts ...Opts) *Manager {
// and returns the resolved release manifest from said configuration.
func (m *Manager) ConfigureComponents(ctx context.Context, conf *image.Configuration, output Output) (rm *resolver.ResolvedManifest, err error) {
if m.rmResolver == nil {
defaultResolver, err := defaultManifestResolver(m.system.FS(), output, m.local)
defaultResolver, err := defaultManifestResolver(m.system.FS(), output, m.cacheDir, m.offline)
if err != nil {
return nil, fmt.Errorf("using default release manifest resolver: %w", err)
}
Expand Down Expand Up @@ -151,7 +165,7 @@ func (m *Manager) ConfigureComponents(ctx context.Context, conf *image.Configura
return rm, nil
}

func defaultManifestResolver(fs vfs.FS, out Output, local bool) (res *resolver.Resolver, err error) {
func defaultManifestResolver(fs vfs.FS, out Output, cacheDir string, offline bool) (res *resolver.Resolver, err error) {
const (
globPattern = "release_manifest*.yaml"
)
Expand All @@ -166,7 +180,7 @@ func defaultManifestResolver(fs vfs.FS, out Output, local bool) (res *resolver.R
return nil, fmt.Errorf("creating release manifest store '%s': %w", manifestsDir, err)
}

extr, err := extractor.New(searchPaths, extractor.WithStore(manifestsDir), extractor.WithLocal(local))
extr, err := extractor.New(searchPaths, extractor.WithStore(manifestsDir), extractor.WithCacheDir(cacheDir), extractor.WithOffline(offline))
if err != nil {
return nil, fmt.Errorf("initializing OCI release manifest extractor: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/config/systemd_sysext.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (m *Manager) unpackExtension(ctx context.Context, extension api.SystemdExte
_ = fs.RemoveAll(tempDir)
}()

unpacker := unpack.NewOCIUnpacker(m.system, extension.Image, unpack.WithLocalOCI(m.local))
unpacker := unpack.NewOCIUnpacker(m.system, extension.Image, unpack.WithCacheDirOCI(m.cacheDir), unpack.WithOfflineOCI(m.offline))
if _, err = unpacker.Unpack(ctx, tempDir); err != nil {
return fmt.Errorf("unpacking extension: %w", err)
}
Expand Down
22 changes: 18 additions & 4 deletions pkg/extractor/extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ import (
type OCIUnpacker interface {
// Unpack unpacks the file system of a given OCI image to the specified destination
// and returns its digest
Unpack(ctx context.Context, uri, dest string, local bool) (digest string, err error)
Unpack(ctx context.Context, uri, dest string, local bool, cacheDir string, offline bool) (digest string, err error)
}

type ociUnpacker struct {
system *sys.System
}

func (o *ociUnpacker) Unpack(ctx context.Context, uri, dest string, local bool) (digest string, err error) {
unpacker := unpack.NewOCIUnpacker(o.system, uri, unpack.WithLocalOCI(local))
func (o *ociUnpacker) Unpack(ctx context.Context, uri, dest string, local bool, cacheDir string, offline bool) (digest string, err error) {
unpacker := unpack.NewOCIUnpacker(o.system, uri, unpack.WithLocalOCI(local), unpack.WithCacheDirOCI(cacheDir), unpack.WithOfflineOCI(offline))
return unpacker.Unpack(ctx, dest)
}

Expand All @@ -59,6 +59,8 @@ type OCIFileExtractor struct {
fs vfs.FS
ctx context.Context
local bool
cacheDir string
offline bool
}

type OCIFileExtractorOpts func(o *OCIFileExtractor)
Expand Down Expand Up @@ -93,6 +95,18 @@ func WithLocal(local bool) OCIFileExtractorOpts {
}
}

func WithCacheDir(cacheDir string) OCIFileExtractorOpts {
return func(r *OCIFileExtractor) {
r.cacheDir = cacheDir
}
}

func WithOffline(offline bool) OCIFileExtractorOpts {
return func(r *OCIFileExtractor) {
r.offline = offline
}
}

func New(searchPaths []string, opts ...OCIFileExtractorOpts) (*OCIFileExtractor, error) {
extr := &OCIFileExtractor{
searchPaths: searchPaths,
Expand Down Expand Up @@ -144,7 +158,7 @@ func (o *OCIFileExtractor) ExtractFrom(uri string) (path string, err error) {
_ = o.fs.RemoveAll(unpackDir)
}()

digest, err := o.unpacker.Unpack(o.ctx, uri, unpackDir, o.local)
digest, err := o.unpacker.Unpack(o.ctx, uri, unpackDir, o.local, o.cacheDir, o.offline)
if err != nil {
return "", fmt.Errorf("unpacking oci image: %w", err)
}
Expand Down
Loading
Loading