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
76 changes: 72 additions & 4 deletions internal/cli/plan.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,86 @@
package cli

import (
"errors"
"context"
"encoding/json"
"fmt"
"io"

"github.com/spf13/cobra"

"github.com/meigma/imgcli/internal/providers"
incusosprovider "github.com/meigma/imgcli/internal/providers/incusos"
imgschemas "github.com/meigma/imgcli/schemas"
"github.com/meigma/imgcli/schemas/core"
)

func newPlanCommand(_ *runtime) *cobra.Command {
func newPlanCommand(rt *runtime) *cobra.Command {
return &cobra.Command{
Use: "plan CONFIG",
Short: "Print the resolved artifact plan",
Args: cobra.ExactArgs(1),
RunE: func(_ *cobra.Command, _ []string) error {
return errors.New("plan command is not implemented yet")
RunE: func(cmd *cobra.Command, args []string) error {
config, err := loadImageConfig(args[0])
if err != nil {
return err
}

plan, err := runIncusOSPlan(cmd.Context(), config)
if err != nil {
return err
}

return printResolvedPlan(rt.opts.stdout(), plan)
},
}
}

func runIncusOSPlan(
ctx context.Context,
config imgschemas.Config,
) (providers.Plan, error) {
provider := incusosprovider.New(*config.Incusos, incusosprovider.Options{})

return provider.Plan(ctx, providers.PlanRequest{
Image: config.Image,
OutputDir: buildOutputDir(config.Output),
})
}

func printResolvedPlan(output io.Writer, plan providers.Plan) error {
encoder := json.NewEncoder(output)
encoder.SetIndent("", " ")
if err := encoder.Encode(resolvedPlanForOutput(plan)); err != nil {
return fmt.Errorf("write resolved artifact plan: %w", err)
}

return nil
}

func resolvedPlanForOutput(plan providers.Plan) core.ResolvedPlan {
resolved := core.ResolvedPlan{
Image: plan.Image,
Version: plan.Version,
OutputDir: plan.OutputDir,
Artifacts: make(map[core.ArtifactKey]core.ResolvedArtifact, len(plan.Artifacts)),
}

for _, artifact := range plan.Artifacts {
resolved.Artifacts[artifact.Key] = core.ResolvedArtifact{
ArtifactKey: artifact.Key,
ImageName: string(plan.Image.Name),
Version: plan.Version,
Variant: artifact.Variant,
Provider: plan.Provider,
Os: artifact.OperatingSystem,
Architecture: artifact.Architecture,
Format: artifact.Format,
MediaType: artifact.MediaType,
Path: artifact.OutputPath,
Labels: artifact.Labels,
Annotations: artifact.Annotations,
}
}

return resolved
}
165 changes: 148 additions & 17 deletions internal/cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,12 @@ func TestInvalidLogSettings(t *testing.T) {

func TestBaseCommands(t *testing.T) {
tests := []struct {
name string
command string
placeholder bool
name string
command string
}{
{
name: "plan",
command: "plan",
placeholder: true,
name: "plan",
command: "plan",
},
{
name: "build",
Expand Down Expand Up @@ -144,22 +142,155 @@ func TestBaseCommands(t *testing.T) {
assert.Empty(t, result.stdout)
assert.Empty(t, result.stderr)
})
}
}

if !tt.placeholder {
continue
func TestPlanCommand(t *testing.T) {
t.Run("prints resolved IncusOS artifact plan", func(t *testing.T) {
clearIMGCLIEnv(t)
outputDir := filepath.Join(t.TempDir(), "out")
cacheDir := filepath.Join(t.TempDir(), "cache")
configPath := writeImageConfig(t, `
apiVersion: "imgcli.meigma.io/v0alpha1"
kind: "ImagePlan"
image: {
name: "test-image"
description: "test image"
}
output: dir: "`+outputDir+`"
incusos: {
defaults: source: channel: "testing"
seed: install: {}
variants: {
secureboot: artifact: {
architecture: "amd64"
format: "raw.gz"
filename: "custom/secureboot.img.gz"
}
default: artifact: {
architecture: "amd64"
format: "raw.gz"
labels: tier: "smoke"
annotations: note: "planned"
}
}
}
`)

t.Run(tt.name+" returns placeholder error", func(t *testing.T) {
clearIMGCLIEnv(t)
result := executeCommand(t, Options{}, "--cache-dir", cacheDir, "plan", configPath)

result := executeCommand(t, Options{}, tt.command, "image.cue")
require.NoError(t, result.err)
assert.Empty(t, result.stderr)
assert.NoDirExists(t, cacheDir)
var plan core.ResolvedPlan
require.NoError(t, json.Unmarshal([]byte(result.stdout), &plan))
assert.Equal(t, core.ResolvedPlan{
Image: core.Image{
Name: core.Name("test-image"),
Description: "test image",
},
OutputDir: outputDir,
Artifacts: map[core.ArtifactKey]core.ResolvedArtifact{
"default": {
ArtifactKey: core.ArtifactKey("default"),
ImageName: "test-image",
Variant: core.VariantName("default"),
Provider: core.ProviderName("incusos"),
Os: "incusos",
Architecture: core.Architecture("amd64"),
Format: core.ArtifactFormat("raw.gz"),
MediaType: "application/gzip",
Path: filepath.Join(outputDir, "test-image-default-amd64.raw.gz"),
Labels: map[string]string{"tier": "smoke"},
Annotations: map[string]string{"note": "planned"},
},
"secureboot": {
ArtifactKey: core.ArtifactKey("secureboot"),
ImageName: "test-image",
Variant: core.VariantName("secureboot"),
Provider: core.ProviderName("incusos"),
Os: "incusos",
Architecture: core.Architecture("amd64"),
Format: core.ArtifactFormat("raw.gz"),
MediaType: "application/gzip",
Path: filepath.Join(outputDir, "custom", "secureboot.img.gz"),
},
},
}, plan)
})

require.Error(t, result.err)
require.ErrorContains(t, result.err, tt.command+" command is not implemented yet")
assert.Empty(t, result.stdout)
assert.Empty(t, result.stderr)
})
}
t.Run("does not require publish configuration", func(t *testing.T) {
clearIMGCLIEnv(t)
t.Setenv("IMGCLI_PUBLISH_PART_SIZE", "1MB")
configPath := writeImageConfig(t, `
apiVersion: "imgcli.meigma.io/v0alpha1"
kind: "ImagePlan"
image: name: "test-image"
incusos: variants: default: artifact: {
architecture: "amd64"
format: "raw.gz"
}
`)

result := executeCommand(t, Options{}, "plan", configPath)

require.NoError(t, result.err)
assert.Empty(t, result.stderr)
assert.NotEmpty(t, result.stdout)
})

t.Run("missing provider fails explicitly", func(t *testing.T) {
clearIMGCLIEnv(t)
configPath := writeImageConfig(t, `
apiVersion: "imgcli.meigma.io/v0alpha1"
kind: "ImagePlan"
image: name: "test-image"
`)

result := executeCommand(t, Options{}, "plan", configPath)

require.Error(t, result.err)
require.ErrorContains(t, result.err, "must specify provider incusos")
assert.Empty(t, result.stdout)
assert.Empty(t, result.stderr)
})

t.Run("unsupported provider fails explicitly", func(t *testing.T) {
clearIMGCLIEnv(t)
configPath := writeImageConfig(t, `
apiVersion: "imgcli.meigma.io/v0alpha1"
kind: "ImagePlan"
image: name: "test-image"
talos: {}
`)

result := executeCommand(t, Options{}, "plan", configPath)

require.Error(t, result.err)
require.ErrorContains(t, result.err, `unsupported provider "talos": only incusos is supported`)
assert.Empty(t, result.stdout)
assert.Empty(t, result.stderr)
})

t.Run("provider planning errors fail before build adapters", func(t *testing.T) {
clearIMGCLIEnv(t)
configPath := writeImageConfig(t, `
apiVersion: "imgcli.meigma.io/v0alpha1"
kind: "ImagePlan"
image: name: "test-image"
incusos: variants: default: artifact: {
architecture: "amd64"
format: "iso"
}
`)

result := executeCommand(t, Options{}, "plan", configPath)

require.Error(t, result.err)
require.ErrorContains(t, result.err, `unsupported incusos artifact format "iso"`)
assert.Empty(t, result.stdout)
assert.Empty(t, result.stderr)
})
}

func TestBuildCommand(t *testing.T) {
Expand Down
3 changes: 0 additions & 3 deletions internal/providers/incusos/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,5 @@ package incusos

import "errors"

// ErrNotImplemented marks provider operations that are scaffolded but not implemented.
var ErrNotImplemented = errors.New("incusos provider operation is not implemented yet")

// ErrImageNotFound indicates that no IncusOS catalog entry matched a query.
var ErrImageNotFound = errors.New("incusos image not found")
Loading