Skip to content
Merged
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
86 changes: 79 additions & 7 deletions internal/cli/build.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package cli

import (
"fmt"

"github.com/spf13/cobra"

"github.com/meigma/imgcli/internal/cache"
"github.com/meigma/imgcli/internal/providers"
incusosprovider "github.com/meigma/imgcli/internal/providers/incusos"
"github.com/meigma/imgcli/internal/providers/incusos/cdn"
"github.com/meigma/imgcli/internal/providers/incusos/imagefile"
"github.com/meigma/imgcli/schemas/core"
)

const defaultBuildOutputDir = "dist"

func newBuildCommand(rt *runtime) *cobra.Command {
return &cobra.Command{
Use: "build CONFIG",
Expand All @@ -19,18 +26,83 @@ func newBuildCommand(rt *runtime) *cobra.Command {
return err
}

catalog := rt.opts.IncusOSCatalog
if catalog == nil {
catalog = cdn.NewClient()
ports, err := rt.incusOSBuildPorts()
if err != nil {
return err
}

provider := incusosprovider.New(*config.Incusos, incusosprovider.Options{
Catalog: catalog,
Output: rt.opts.stdout(),
Catalog: ports.catalog,
Downloader: ports.downloader,
SeedBuilder: ports.seedBuilder,
ImageInjector: ports.imageInjector,
})

_, err = provider.Build(cmd.Context(), providers.BuildRequest{})
return err
result, err := provider.Build(cmd.Context(), providers.BuildRequest{
Plan: providers.Plan{
Image: config.Image,
},
OutputDir: buildOutputDir(config.Output),
})
if err != nil {
return err
}

for _, artifact := range result.Artifacts {
if _, err := fmt.Fprintln(rt.opts.stdout(), artifact.Path); err != nil {
return fmt.Errorf("write build artifact path: %w", err)
}
}

return nil
},
}
}

type incusOSBuildPorts struct {
catalog incusosprovider.Catalog
downloader incusosprovider.Downloader
seedBuilder incusosprovider.SeedBuilder
imageInjector incusosprovider.ImageInjector
}

func (rt *runtime) incusOSBuildPorts() (incusOSBuildPorts, error) {
ports := incusOSBuildPorts{
catalog: rt.opts.IncusOSCatalog,
downloader: rt.opts.IncusOSDownloader,
seedBuilder: rt.opts.IncusOSSeedBuilder,
imageInjector: rt.opts.IncusOSImageInjector,
}

if ports.catalog == nil || ports.downloader == nil {
cacheStore, err := cache.NewDiskStore()
if err != nil {
return incusOSBuildPorts{}, fmt.Errorf("configure incusos cache: %w", err)
}

client := cdn.NewClient(cdn.WithCacheService(cacheStore))
if ports.catalog == nil {
ports.catalog = client
}
if ports.downloader == nil {
ports.downloader = client
}
}

if ports.seedBuilder == nil {
ports.seedBuilder = incusosprovider.SeedArchiveBuilder{}
}
if ports.imageInjector == nil {
ports.imageInjector = imagefile.Injector{}
}

return ports, nil
}

func buildOutputDir(output *core.OutputDefaults) string {
if output == nil || output.Dir == "" {
return defaultBuildOutputDir
}

return output.Dir
}
9 changes: 9 additions & 0 deletions internal/cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ type Options struct {

// IncusOSCatalog resolves IncusOS source images. Nil selects the default CDN catalog.
IncusOSCatalog incusos.Catalog

// IncusOSDownloader retrieves IncusOS source images. Nil selects the default CDN downloader.
IncusOSDownloader incusos.Downloader

// IncusOSSeedBuilder creates IncusOS seed archives. Nil selects the default seed builder.
IncusOSSeedBuilder incusos.SeedBuilder

// IncusOSImageInjector writes IncusOS seed archives into source images. Nil selects the default image injector.
IncusOSImageInjector incusos.ImageInjector
}

func (o Options) version() string {
Expand Down
83 changes: 78 additions & 5 deletions internal/cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,33 +192,55 @@ talos: {}
assert.Empty(t, result.stderr)
})

t.Run("prints resolved IncusOS image URL", func(t *testing.T) {
t.Run("prints customized IncusOS artifact path", func(t *testing.T) {
clearIMGCLIEnv(t)
outputDir := filepath.Join(t.TempDir(), "out")
configPath := writeImageConfig(t, `
apiVersion: "imgcli.meigma.io/v0alpha1"
kind: "ImagePlan"
image: name: "test-image"
output: dir: "`+outputDir+`"
incusos: {
defaults: source: channel: "testing"
seed: install: {}
variants: default: {
source: version: "202604261712"
artifact: {
architecture: "amd64"
format: "raw"
format: "raw.gz"
}
}
}
`)
catalog := &testCatalog{
asset: incusos.ImageAsset{
URL: "https://example.invalid/os/202604261712/x86_64/IncusOS_202604261712.img.gz",
URL: "https://example.invalid/os/202604261712/x86_64/IncusOS_202604261712.img.gz",
SHA256: "source-sha",
Size: 42,
},
}
downloader := &testDownloader{
image: incusos.DownloadedImage{
Path: "/cache/source.img.gz",
SHA256: "source-sha",
Size: 42,
},
}
seedBuilder := &testSeedBuilder{
seed: incusos.SeedArchive{Data: []byte("seed")},
}
injector := &testImageInjector{}

result := executeCommand(t, Options{IncusOSCatalog: catalog}, "build", configPath)
result := executeCommand(t, Options{
IncusOSCatalog: catalog,
IncusOSDownloader: downloader,
IncusOSSeedBuilder: seedBuilder,
IncusOSImageInjector: injector,
}, "build", configPath)

require.NoError(t, result.err)
assert.Equal(t, "https://example.invalid/os/202604261712/x86_64/IncusOS_202604261712.img.gz\n", result.stdout)
wantOutputPath := filepath.Join(outputDir, "test-image-default-amd64.raw.gz")
assert.Equal(t, wantOutputPath+"\n", result.stdout)
assert.Empty(t, result.stderr)
require.Len(t, catalog.queries, 1)
assert.Equal(t, incusos.ImageQuery{
Expand All @@ -227,6 +249,10 @@ incusos: {
Architecture: core.Architecture("amd64"),
Type: incusos.ImageTypeRaw,
}, catalog.queries[0])
assert.Equal(t, []incusos.ImageAsset{catalog.asset}, downloader.assets)
assert.Len(t, seedBuilder.configs, 1)
require.Len(t, injector.calls, 1)
assert.Equal(t, wantOutputPath, injector.calls[0].outputPath)
})
}

Expand Down Expand Up @@ -283,3 +309,50 @@ func (c *testCatalog) ResolveImage(_ context.Context, query incusos.ImageQuery)
c.queries = append(c.queries, query)
return c.asset, nil
}

type testDownloader struct {
image incusos.DownloadedImage
assets []incusos.ImageAsset
}

func (d *testDownloader) DownloadImage(_ context.Context, asset incusos.ImageAsset) (incusos.DownloadedImage, error) {
d.assets = append(d.assets, asset)
image := d.image
image.Asset = asset
return image, nil
}

type testSeedBuilder struct {
seed incusos.SeedArchive
configs []incusos.Config
}

func (b *testSeedBuilder) BuildSeed(_ context.Context, config incusos.Config) (incusos.SeedArchive, error) {
b.configs = append(b.configs, config)
return b.seed, nil
}

type testImageInjector struct {
calls []testInjectCall
}

type testInjectCall struct {
image incusos.DownloadedImage
seed incusos.SeedArchive
outputPath string
}

func (i *testImageInjector) InjectSeed(
_ context.Context,
image incusos.DownloadedImage,
seed incusos.SeedArchive,
outputPath string,
) (incusos.CustomizedImage, error) {
i.calls = append(i.calls, testInjectCall{image: image, seed: seed, outputPath: outputPath})
return incusos.CustomizedImage{
Source: image,
Path: outputPath,
Size: 99,
SHA256: "custom-sha",
}, nil
}
Loading