From 14d21a0aaef8c6095712dbe941df59025ba49594 Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Tue, 21 Apr 2026 10:20:04 +0100 Subject: [PATCH 1/3] feat(images): List direct pushed images Signed-off-by: Justin Chadwell --- cmd/unikraft/testdata/TestGolden/images/help | 4 +- go.mod | 4 +- go.sum | 4 +- internal/cmd/images.go | 298 ++++++++++++++----- 4 files changed, 234 insertions(+), 76 deletions(-) diff --git a/cmd/unikraft/testdata/TestGolden/images/help b/cmd/unikraft/testdata/TestGolden/images/help index e691ef01..f29e1de0 100644 --- a/cmd/unikraft/testdata/TestGolden/images/help +++ b/cmd/unikraft/testdata/TestGolden/images/help @@ -7,12 +7,12 @@ stdout: unikraft images [flags] Resources: + list, ls + List images. get, inspect, show Inspect a image. delete, rm, remove Remove a image. - list, ls - List images. copy Copy images. diff --git a/go.mod b/go.mod index 7c54b090..798ad22e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module unikraft.com/cli -go 1.26.0 +go 1.26.1 require ( charm.land/bubbles/v2 v2.1.0 @@ -43,7 +43,7 @@ require ( gotest.tools/v3 v3.5.2 mvdan.cc/sh/v3 v3.13.1 sigs.k8s.io/yaml v1.6.0 - unikraft.com/cloud/sdk v0.0.0-20260416133315-be4aec303a89 + unikraft.com/cloud/sdk v0.0.0-20260429110911-ff0c948a8c0d unikraft.com/x/colors v0.0.0-20260313145522-d793c36d706e unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f unikraft.com/x/fingerprint v0.0.0-20260126094137-ab6e717e5679 diff --git a/go.sum b/go.sum index 341603be..b6f30e3a 100644 --- a/go.sum +++ b/go.sum @@ -495,8 +495,8 @@ sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= tailscale.com v1.94.1 h1:0dAst/ozTuFkgmxZULc3oNwR9+qPIt5ucvzH7kaM0Jw= tailscale.com v1.94.1/go.mod h1:gLnVrEOP32GWvroaAHHGhjSGMPJ1i4DvqNwEg+Yuov4= -unikraft.com/cloud/sdk v0.0.0-20260416133315-be4aec303a89 h1:hMNR+ulLXyGiGFoRnOeKkfAgp/bH5Fzz4nrU36ZEgcE= -unikraft.com/cloud/sdk v0.0.0-20260416133315-be4aec303a89/go.mod h1:mB0KNJFoeiV8zucDtuAFGW16tUsusA/ZHShhqbqhA5Q= +unikraft.com/cloud/sdk v0.0.0-20260429110911-ff0c948a8c0d h1:rxSn/SibpN/lzX83H7XBBgasbqxUsmeM6F/iDaY8WVI= +unikraft.com/cloud/sdk v0.0.0-20260429110911-ff0c948a8c0d/go.mod h1:S7aGf6JP7grJSG0i+cmePnuPkuRCEbfWy3vWKXksi44= unikraft.com/x/colors v0.0.0-20260313145522-d793c36d706e h1:C/V6l4ut5XpcVTN5CvnskRv6NHDbyIeLdgFVLEJ9BIE= unikraft.com/x/colors v0.0.0-20260313145522-d793c36d706e/go.mod h1:SVlAGfyQ7MwJom7m9M2w83+TrO+nJoiLxeduJAxagEo= unikraft.com/x/filters v0.0.0-20260416164455-ec39ae908f3f h1:v6pitpzsBnOjyzDIW0/YAEHHSI6cNOw4QOwQV0uD+dc= diff --git a/internal/cmd/images.go b/internal/cmd/images.go index dca1baed..f9c0d4cc 100644 --- a/internal/cmd/images.go +++ b/internal/cmd/images.go @@ -17,13 +17,16 @@ import ( "github.com/distribution/reference" "github.com/opencontainers/go-digest" "unikraft.com/cloud/sdk/controlplane" + "unikraft.com/cloud/sdk/platform" "unikraft.com/cloud/sdk/platform/group" + "unikraft.com/x/joinerrgroup" "unikraft.com/x/kingkong" "unikraft.com/x/log" "unikraft.com/x/ptr" imagespec "unikraft.com/x/image-spec" + "unikraft.com/cli/internal/config" "unikraft.com/cli/internal/images" "unikraft.com/cli/internal/multimetro" "unikraft.com/cli/internal/resource" @@ -34,17 +37,13 @@ import ( type ImagesCmd struct { cmd.ResourceCmd[ImageEntry] + cmd.ListableResourceCmd[ImageEntry] cmd.GettableResourceCmd[Image] cmd.DeletableResourceCmd[Image] - List ImagesListCmd `cmd:"" help:"List images." aliases:"ls"` Copy ImagesCopyCmd `cmd:"" help:"Copy images."` } -type ImagesListCmd struct { - cmd.ResourceListCmd[ImageEntry] -} - type Image struct { Ref types.ImageRef[reference.Named] `field:",short"` Digest digest.Digest `field:",long"` @@ -91,7 +90,7 @@ func (i Image) Key() resource.Key { } func (i Image) Raw() any { - return nil // NOTE: no platform API response associated + return i.Image.Descriptor } func (i Image) Fields(ctx context.Context) ([]resource.Field, error) { @@ -246,14 +245,15 @@ func (Image) Examples() map[cmd.CmdType][]kingkong.Example { } type ImageEntry struct { - Ref types.ImageRef[reference.NamedTagged] `field:",short"` - Digest digest.Digest `field:",short"` + Ref types.ImageRef[reference.Named] `field:",short"` + Digest digest.Digest `field:",short"` Namespace string Canonical reference.Canonical `field:"-"` - Image controlplane.Image `field:"-" json:"image"` + controlplaneImage *controlplane.Image + platformImage *platform.Image } func (ImageEntry) Type() resource.Type { @@ -268,15 +268,17 @@ func (i ImageEntry) Key() resource.Key { } func (i ImageEntry) Raw() any { - return i.Image + if i.controlplaneImage != nil { + return i.controlplaneImage + } + if i.platformImage != nil { + return i.platformImage + } + return nil } func (i ImageEntry) Fields(ctx context.Context) ([]resource.Field, error) { - result, err := resource.FieldsFromStruct(i) - if err != nil { - return nil, err - } - return result, nil + return resource.FieldsFromStruct(i) } func (ImageEntry) Examples() map[cmd.CmdType][]kingkong.Example { @@ -289,28 +291,85 @@ func (ImageEntry) List(ctx context.Context) ([]resource.Resource, error) { return nil, err } - log.G(ctx).Trace().Msg("listing images") - resp, err := client.ListImages(ctx, controlplane.ListImagesOpts{Details: new(true)}) - if err != nil { + var controlplaneResults, platformResults []resource.Resource + + eg, ctx := joinerrgroup.WithContext(ctx) + eg.Go(func() error { + log.G(ctx).Trace().Msg("listing images from controlplane") + resp, err := client.ListImages(ctx, controlplane.ListImagesOpts{Details: new(true)}) + if err != nil { + return err + } + if resp.Data != nil { + for _, image := range resp.Data.Images { + entries, err := ImageEntry{}.loadFromControlplane(image) + if err != nil { + return err + } + for _, entry := range entries { + controlplaneResults = append(controlplaneResults, entry) + } + } + } + return nil + }) + eg.Go(func() error { + var err error + platformResults, err = listPlatformImages(ctx) + return err + }) + if err := eg.Wait(); err != nil { return nil, err } - if resp.Data == nil { - return nil, nil + + seen := make(map[string]struct{}, len(controlplaneResults)) + for _, r := range controlplaneResults { + seen[r.(ImageEntry).Ref.Reference.String()] = struct{}{} + } + results := controlplaneResults + for _, r := range platformResults { + ref := r.(ImageEntry).Ref.Reference.String() + if _, ok := seen[ref]; ok { + continue + } + seen[ref] = struct{}{} + results = append(results, r) } - var results []resource.Resource - var errs []error - for _, image := range resp.Data.Images { - entries, err := ImageEntry{}.load(image) + return results, nil +} + +func listPlatformImages(ctx context.Context) ([]resource.Resource, error) { + g, err := multimetro.NewClient(ctx) + if err != nil { + return nil, err + } + + return group.CollectAllSlices(ctx, g, func(ctx context.Context, c multimetro.MetroClient) ([]resource.Resource, error) { + log.G(ctx).Trace().Msg("listing images from image-store") + + resp, err := c.GetImageStore(ctx, nil, platform.GetImageStoreOpts{}) if err != nil { - errs = append(errs, err) - continue + log.G(ctx).Trace().Err(err).Msg("skipping image-store listing") + return nil, nil } - for _, entry := range entries { - results = append(results, entry) + + var results []resource.Resource + var errs []error + if resp.Data != nil { + for _, image := range resp.Data.Images { + entries, err := ImageEntry{}.loadFromPlatform(image, &c.Metro) + if err != nil { + errs = append(errs, err) + continue + } + for _, entry := range entries { + results = append(results, entry) + } + } } - } - return results, errors.Join(errs...) + return results, errors.Join(errs...) + }) } func (ImageEntry) Get(ctx context.Context, keys []string) ([]resource.Resource, error) { @@ -329,42 +388,58 @@ func (ImageEntry) Get(ctx context.Context, keys []string) ([]resource.Resource, } log.G(ctx).Trace().Msg("getting images") - details := true - resp, err := client.ListImages(ctx, controlplane.ListImagesOpts{Details: &details}) + resp, err := client.ListImages(ctx, controlplane.ListImagesOpts{Details: new(true)}) if err != nil { return nil, err } - if resp.Data == nil { - refs := make(group.Refs, 0, len(normalizedKeys)) - for _, key := range normalizedKeys { - refs = append(refs, group.Ref{Name: key}) - } - return nil, group.ErrRefNotFound{Refs: refs} - } found := make(map[string]struct{}, len(normalizedKeys)) var results []resource.Resource var errs []error - for _, image := range resp.Data.Images { - entries, err := ImageEntry{}.load(image) - if err != nil { - errs = append(errs, err) - continue + if resp.Data != nil { + for _, image := range resp.Data.Images { + entries, err := ImageEntry{}.loadFromControlplane(image) + if err != nil { + errs = append(errs, err) + continue + } + for _, key := range normalizedKeys { + if _, ok := found[key]; ok { + continue + } + for _, entry := range entries { + matchRef := reference.Named(entry.Ref.Reference) + if entry.Canonical != nil { + matchRef = entry.Canonical + } + if xreference.MatchNamed(matchRef, key) { + found[key] = struct{}{} + results = append(results, entry) + break + } + } + } } + } + + platformResults, platformErr := listPlatformImages(ctx) + if platformErr != nil { + errs = append(errs, platformErr) + } + for _, r := range platformResults { + entry := r.(ImageEntry) for _, key := range normalizedKeys { if _, ok := found[key]; ok { continue } - for _, entry := range entries { - matchRef := reference.Named(entry.Ref.Reference) - if entry.Canonical != nil { - matchRef = entry.Canonical - } - if xreference.MatchNamed(matchRef, key) { - found[key] = struct{}{} - results = append(results, entry) - break - } + matchRef := reference.Named(entry.Ref.Reference) + if entry.Canonical != nil { + matchRef = entry.Canonical + } + if xreference.MatchNamed(matchRef, key) { + found[key] = struct{}{} + results = append(results, r) + break } } } @@ -383,7 +458,7 @@ func (ImageEntry) Get(ctx context.Context, keys []string) ([]resource.Resource, return results, errors.Join(errors.Join(errs...), missingErr) } -func (ImageEntry) load(image controlplane.Image) ([]ImageEntry, error) { +func (ImageEntry) loadFromControlplane(image controlplane.Image) ([]ImageEntry, error) { name := strings.TrimSpace(ptr.ZeroIfNil(image.Name)) if name == "" { return nil, fmt.Errorf("image has no name") @@ -393,8 +468,8 @@ func (ImageEntry) load(image controlplane.Image) ([]ImageEntry, error) { return nil, fmt.Errorf("could not parse image name %q: %w", name, err) } var baseDigest digest.Digest - if baseDigested, ok := base.(reference.Digested); ok { - baseDigest = baseDigested.Digest() + if d, ok := base.(reference.Digested); ok { + baseDigest = d.Digest() } base = reference.TrimNamed(base) @@ -440,30 +515,32 @@ func (ImageEntry) load(image controlplane.Image) ([]ImageEntry, error) { } if len(tagged) == 0 { - return nil, fmt.Errorf("image has no tags") + return nil, nil } - // move latest to front if present - idx := slices.IndexFunc(tagged, func(t reference.NamedTagged) bool { + // Move latest to front if present. + if idx := slices.IndexFunc(tagged, func(t reference.NamedTagged) bool { return t.Tag() == "latest" - }) - if idx > 0 { + }); idx > 0 { latest := tagged[idx] - tagged = append(tagged[:idx], tagged[idx+1:]...) - tagged = append([]reference.NamedTagged{latest}, tagged...) + tagged = slices.Insert(slices.Delete(tagged, idx, idx+1), 0, latest) + } + + if len(tagged) == 0 { + return nil, nil } results := make([]ImageEntry, 0, len(tagged)) for _, tag := range tagged { - result := ImageEntry{ - Image: image, - } - tagDigest := tagDigests[tag.Tag()] if tagDigest == "" { tagDigest = baseDigest } - result.Digest = tagDigest + + result := ImageEntry{ + controlplaneImage: &image, + Digest: tagDigest, + } if tagDigest != "" { canonical, err := reference.WithDigest(tag, tagDigest) if err != nil { @@ -471,14 +548,95 @@ func (ImageEntry) load(image controlplane.Image) ([]ImageEntry, error) { } result.Canonical = canonical } - result.Ref.Reference = tag if ns, _, ok := strings.Cut(reference.Path(tag), "/"); ok { result.Namespace = ns } results = append(results, result) } + return results, nil +} + +func (ImageEntry) loadFromPlatform(image platform.Image, metro *config.Metro) ([]ImageEntry, error) { + url := strings.TrimSpace(ptr.ZeroIfNil(image.Url)) + if url == "" { + return nil, fmt.Errorf("platform image has no url") + } + parsed, err := images.ParseNormalizedNamedMetro(metro, url) + if err != nil { + return nil, fmt.Errorf("could not parse platform image url %q: %w", url, err) + } + + var baseDigest digest.Digest + if d, ok := parsed.(reference.Digested); ok { + baseDigest = d.Digest() + } + base, err := reference.ParseNamed(metro.Index().Host + "/" + reference.Path(parsed)) + if err != nil { + return nil, fmt.Errorf("could not construct platform image ref: %w", err) + } + + var tagged []reference.NamedTagged + for _, tag := range image.Tags { + if strings.HasPrefix(tag, "sha256:") { + // Digest entry, not a tag. + continue + } + + var tagVal string + if strings.Contains(tag, ":") { + // Legacy format: "image:tag" + _, tagVal, _ = strings.Cut(tag, ":") + if strings.HasPrefix(tagVal, "sha256:") { + continue + } + } else { + // Raw tag format from /v1/image-store + tagVal = tag + } + if tagVal == "" { + continue + } + ref, err := reference.WithTag(base, tagVal) + if err != nil { + return nil, fmt.Errorf("could not parse platform image tag %q: %w", tag, err) + } + tagged = append(tagged, ref) + } + + // Move latest to front if present. + if idx := slices.IndexFunc(tagged, func(t reference.NamedTagged) bool { + return t.Tag() == "latest" + }); idx > 0 { + latest := tagged[idx] + tagged = slices.Insert(slices.Delete(tagged, idx, idx+1), 0, latest) + } + + if len(tagged) == 0 { + // No tags means the image is dangling; skip it. + return nil, nil + } + + results := make([]ImageEntry, 0, len(tagged)) + for _, tag := range tagged { + result := ImageEntry{ + platformImage: &image, + Digest: baseDigest, + } + if baseDigest != "" { + canonical, err := reference.WithDigest(tag, baseDigest) + if err != nil { + return nil, fmt.Errorf("could not create image canonical reference: %w", err) + } + result.Canonical = canonical + } + result.Ref.Reference = tag + if ns, _, ok := strings.Cut(reference.Path(tag), "/"); ok { + result.Namespace = ns + } + results = append(results, result) + } return results, nil } From 7284c07f374a59112ec121930c47889879ece81f Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Wed, 29 Apr 2026 11:35:04 +0100 Subject: [PATCH 2/3] test: Allow distinguishing error checks We should assert errors happen in most cases we care about, but there are some where we don't really care if an error happens or not. Signed-off-by: Justin Chadwell --- cmd/unikraft/auth_test.go | 4 ++-- cmd/unikraft/help_test.go | 12 ++++++------ cmd/unikraft/main_test.go | 16 +++++++++++++--- cmd/unikraft/volumes_test.go | 6 +++--- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/cmd/unikraft/auth_test.go b/cmd/unikraft/auth_test.go index 7eefd29a..a1808b6e 100644 --- a/cmd/unikraft/auth_test.go +++ b/cmd/unikraft/auth_test.go @@ -29,8 +29,8 @@ func authTests(t *testing.T, r *testRunner) { {args: []string{unikraftCmd, "profile", "list"}}, {args: []string{unikraftCmd, "metro", "list"}}, {args: []string{unikraftCmd, "logout"}}, - {args: []string{unikraftCmd, "profile", "list"}, allowErr: true}, - {args: []string{unikraftCmd, "metro", "list"}, allowErr: true}, + {args: []string{unikraftCmd, "profile", "list"}, err: errMaybe}, + {args: []string{unikraftCmd, "metro", "list"}, err: errMaybe}, }) }) } diff --git a/cmd/unikraft/help_test.go b/cmd/unikraft/help_test.go index 438ae886..7f1a5e8a 100644 --- a/cmd/unikraft/help_test.go +++ b/cmd/unikraft/help_test.go @@ -10,7 +10,7 @@ import "testing" func helpTests(t *testing.T, r *testRunner) { t.Run("empty", func(t *testing.T) { r.run(t, []command{ - {args: []string{unikraftCmd}, allowErr: true}, + {args: []string{unikraftCmd}, err: errYes}, }) }) t.Run("version", func(t *testing.T) { @@ -25,19 +25,19 @@ func helpTests(t *testing.T, r *testRunner) { }) t.Run("invalid/arg", func(t *testing.T) { r.run(t, []command{ - {args: []string{unikraftCmd, "invalid"}, allowErr: true}, + {args: []string{unikraftCmd, "invalid"}, err: errYes}, }) }) t.Run("invalid/help", func(t *testing.T) { r.run(t, []command{ - {args: []string{unikraftCmd, "--help", "--bad-flag"}, allowErr: true}, - {args: []string{unikraftCmd, "--help", "bad-arg"}, allowErr: true}, + {args: []string{unikraftCmd, "--help", "--bad-flag"}, err: errYes}, + {args: []string{unikraftCmd, "--help", "bad-arg"}, err: errYes}, }) }) t.Run("invalid/logs", func(t *testing.T) { r.run(t, []command{ - {args: []string{unikraftCmd, "--log-type=json", "invalid"}, allowErr: true}, - {args: []string{unikraftCmd, "--log-level=fatal", "invalid"}, allowErr: true}, + {args: []string{unikraftCmd, "--log-type=json", "invalid"}, err: errYes}, + {args: []string{unikraftCmd, "--log-level=fatal", "invalid"}, err: errYes}, }) }) } diff --git a/cmd/unikraft/main_test.go b/cmd/unikraft/main_test.go index 37216762..aea3cc62 100644 --- a/cmd/unikraft/main_test.go +++ b/cmd/unikraft/main_test.go @@ -56,10 +56,18 @@ type testRunner struct { // command represents a single command to execute in a test. type command struct { args []string - allowErr bool + err commandErr captureEnv string } +type commandErr int + +const ( + errNo commandErr = iota // command must succeed + errMaybe // command may fail (either outcome is acceptable) + errYes // command must fail +) + // testBuilder provides a fluent interface for configuring and running tests. type testBuilder struct { runner *testRunner @@ -235,9 +243,8 @@ func (b *testBuilder) run(t *testing.T, commands []command) { } var exitErr *exec.ExitError var exitCode int - if errors.As(err, &exitErr) && command.allowErr { + if errors.As(err, &exitErr) && command.err >= errMaybe { exitCode = exitErr.ExitCode() - // ignore exit errors for help commands err = nil } require.NoError(t, err, "command %q failed\nstdout:\n%s\nstderr:\n%s", @@ -245,6 +252,9 @@ func (b *testBuilder) run(t *testing.T, commands []command) { stdout.String(), stderr.String(), ) + if command.err == errYes { + require.NotZero(t, exitCode, "command %q was expected to fail but succeeded", strings.Join(args, " ")) + } report := report{ args: command.args, diff --git a/cmd/unikraft/volumes_test.go b/cmd/unikraft/volumes_test.go index b709205b..88a617f7 100644 --- a/cmd/unikraft/volumes_test.go +++ b/cmd/unikraft/volumes_test.go @@ -68,21 +68,21 @@ func volumesTests(t *testing.T, r *testRunner) { // Offline: missing --source errors before any network call. t.Run("missing-source", func(t *testing.T) { r.run(t, []command{ - {args: []string{unikraftCmd, "volume", "import", "my-volume"}, allowErr: true}, + {args: []string{unikraftCmd, "volume", "import", "my-volume"}, err: errYes}, }) }) // Offline: port below the allowed range errors before any network call. t.Run("invalid-port", func(t *testing.T) { r.run(t, []command{ - {args: []string{unikraftCmd, "volume", "import", "my-volume", "--source", ".", "--port", "80"}, allowErr: true}, + {args: []string{unikraftCmd, "volume", "import", "my-volume", "--source", ".", "--port", "80"}, err: errYes}, }) }) // Offline: port above the allowed range errors before any network call. t.Run("invalid-port-high", func(t *testing.T) { r.run(t, []command{ - {args: []string{unikraftCmd, "volume", "import", "my-volume", "--source", ".", "--port", "99999"}, allowErr: true}, + {args: []string{unikraftCmd, "volume", "import", "my-volume", "--source", ".", "--port", "99999"}, err: errYes}, }) }) From b874e215fef4d1a3d73422824038bbc7bbfd1a5e Mon Sep 17 00:00:00 2001 From: Justin Chadwell Date: Wed, 29 Apr 2026 11:36:57 +0100 Subject: [PATCH 3/3] test: Add direct push testing Signed-off-by: Justin Chadwell --- cmd/unikraft/build_test.go | 71 +++++++++--- cmd/unikraft/main_test.go | 5 + .../testdata/TestGolden/build/busybox/cpio | 51 --------- .../TestGolden/build/busybox/cpio/direct-push | 103 +++++++++++++++++ .../TestGolden/build/busybox/cpio/registry | 107 ++++++++++++++++++ .../testdata/TestGolden/build/busybox/erofs | 51 --------- .../build/busybox/erofs/direct-push | 103 +++++++++++++++++ .../TestGolden/build/busybox/erofs/registry | 107 ++++++++++++++++++ 8 files changed, 479 insertions(+), 119 deletions(-) delete mode 100644 cmd/unikraft/testdata/TestGolden/build/busybox/cpio create mode 100644 cmd/unikraft/testdata/TestGolden/build/busybox/cpio/direct-push create mode 100644 cmd/unikraft/testdata/TestGolden/build/busybox/cpio/registry delete mode 100644 cmd/unikraft/testdata/TestGolden/build/busybox/erofs create mode 100644 cmd/unikraft/testdata/TestGolden/build/busybox/erofs/direct-push create mode 100644 cmd/unikraft/testdata/TestGolden/build/busybox/erofs/registry diff --git a/cmd/unikraft/build_test.go b/cmd/unikraft/build_test.go index c3fe32be..5554814a 100644 --- a/cmd/unikraft/build_test.go +++ b/cmd/unikraft/build_test.go @@ -18,23 +18,40 @@ func buildTests(t *testing.T, r *testRunner) { }) }) - var busybox, metroName string + var metroName string + type variant struct { + name string + image string + } + var variants []variant if r.cfg != nil { metroName = r.cfg.MetroName - busybox = fmt.Sprintf("%s/busybox-e2e:$UNIQ_IMAGE", r.cfg.Profile.Organization) - // this is what we'd use to test direct push - // busybox := fmt.Sprintf("%s/%s/busybox-e2e:$UNIQ_IMAGE", cfg.Metro.Index().Host, cfg.Profile.Organization) + variants = []variant{ + { + name: "registry", + image: fmt.Sprintf("%s/busybox-e2e:$UNIQ_IMAGE", r.cfg.Profile.Organization), + }, + { + name: "direct-push", + image: fmt.Sprintf("%s/%s/busybox-e2e:$UNIQ_IMAGE", r.cfg.Metro.Index().Host, r.cfg.Profile.Organization), + }, + } } t.Run("busybox", func(t *testing.T) { + if r.cfg == nil { + t.Skip("busybox tests require online config") + } for _, format := range []string{"cpio", "erofs"} { t.Run(format, func(t *testing.T) { - r. - online(). - withCleaners(buildCleaners). - withContext(map[string]string{ - "Dockerfile": ` + for _, v := range variants { + t.Run(v.name, func(t *testing.T) { + r. + online(). + withCleaners(buildCleaners). + withContext(map[string]string{ + "Dockerfile": ` FROM busybox:latest RUN echo "unikraft-e2e" > /etc/unikraft-e2e COPY < version=vX.Y.Z - -$ unikraft run --name test-$UNIQ_INST --metro test --output quiet --image test/busybox-e2e:$UNIQ_IMAGE - -stdout: - test/test- - -$ unikraft instance wait --until state==stopped --timeout 10s test-$UNIQ_INST - -stdout: - metro: test - name: test- - uuid: 12345678-1234-1234-1234-123456789abc - state: stopped - image: test/busybox-e2e - resources: - memory: 128MiB - vcpus: 1 - networks: - - uuid: 12345678-1234-1234-1234-123456789abc - private-ip: 10.X.X.X - mac: aa:bb:cc:dd:ee:ff - timestamps: - created: RELATIVE_TIME - stop: - reason: app exit - exit-code: 0 - -$ unikraft instance logs test-$UNIQ_INST - -stdout: - test- ┏ [ 0.000000] RDSEED32 is broken. Disabling the corresponding CPUID bit. - test- │ == BEGIN /etc/unikraft-e2e == - test- │ unikraft-e2e - test- │ == END /etc/unikraft-e2e == - test- │ == BEGIN ls /etc/unikraft-e2e == - test- │ /etc/unikraft-e2e - test- │ == END ls /etc/unikraft-e2e == - test- │ == BEGIN status == - test- │ UNIKRAFT_E2E_OK - test- │ == END status == - test- │ Application exited with 0x0 (exit code: 0) - test- │ [ 0.000000] reboot: Restarting system - -$ unikraft instance delete test-$UNIQ_INST - -stdout: - test- diff --git a/cmd/unikraft/testdata/TestGolden/build/busybox/cpio/direct-push b/cmd/unikraft/testdata/TestGolden/build/busybox/cpio/direct-push new file mode 100644 index 00000000..26bfc560 --- /dev/null +++ b/cmd/unikraft/testdata/TestGolden/build/busybox/cpio/direct-push @@ -0,0 +1,103 @@ +$ unikraft build . --output index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stderr: + │ found buildkit addr= version=vX.Y.Z + +$ unikraft image inspect index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stdout: + ref: index.unikraft.test/test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + config: + platform: kraftcloud/x86_64 + cmd: ["sh", "/entrypoint.sh"] + env: + PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + metadata: + created: YYYY-MM-DD HH:MM:SS +0000 UTC + kernel: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + media-type: application/vnd.oci.image.layer.v1.tar + annotations: + org.unikraft.kernel.image: /unikraft/bin/kernel + size: X.XXXMiB + initrd: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + media-type: application/vnd.oci.image.layer.v1.tar + annotations: + org.unikraft.kernel.initrd: /unikraft/bin/initrd + size: X.XXXMiB + +$ unikraft image ls index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE -okv + +stdout: + ref: index.unikraft.test/test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + +$ unikraft run --name test-$UNIQ_INST --metro test --output quiet --image index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stdout: + test/test- + +$ unikraft instance wait --until state==stopped --timeout 10s test-$UNIQ_INST + +stdout: + metro: test + name: test- + uuid: 12345678-1234-1234-1234-123456789abc + state: stopped + image: index.unikraft.test/test/busybox-e2e + resources: + memory: X.XXXMiB + vcpus: 1 + networks: + - uuid: 12345678-1234-1234-1234-123456789abc + private-ip: 10.X.X.X + mac: aa:bb:cc:dd:ee:ff + timestamps: + created: RELATIVE_TIME + stop: + reason: app exit + exit-code: 0 + +$ unikraft instance logs test-$UNIQ_INST + +stdout: + test- ┏ [ 0.000000] RDSEED32 is broken. Disabling the corresponding CPUID bit. + test- │ == BEGIN /etc/unikraft-e2e == + test- │ unikraft-e2e + test- │ == END /etc/unikraft-e2e == + test- │ == BEGIN ls /etc/unikraft-e2e == + test- │ /etc/unikraft-e2e + test- │ == END ls /etc/unikraft-e2e == + test- │ == BEGIN status == + test- │ UNIKRAFT_E2E_OK + test- │ == END status == + test- │ Application exited with 0x0 (exit code: 0) + test- │ [ 0.000000] reboot: Restarting system + +$ unikraft instance delete test-$UNIQ_INST + +stdout: + test- + +$ unikraft image delete index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stdout: + index.unikraft.test/test/busybox-e2e: + +$ unikraft image inspect index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stderr: + │ + │ error: + │ failed to resolve image "index.unikraft.test/test/busybox-e2e:": index.unikraft.test/test/busybox-e2e:: not found + │ + +exit code: 1 + +$ unikraft image ls index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE -okv + +stdout: + ref: index.unikraft.test/test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 diff --git a/cmd/unikraft/testdata/TestGolden/build/busybox/cpio/registry b/cmd/unikraft/testdata/TestGolden/build/busybox/cpio/registry new file mode 100644 index 00000000..6bcb3560 --- /dev/null +++ b/cmd/unikraft/testdata/TestGolden/build/busybox/cpio/registry @@ -0,0 +1,107 @@ +$ unikraft build . --output test/busybox-e2e:$UNIQ_IMAGE + +stderr: + │ found buildkit addr= version=vX.Y.Z + +$ unikraft image inspect test/busybox-e2e:$UNIQ_IMAGE + +stdout: + ref: test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + config: + platform: kraftcloud/x86_64 + cmd: ["sh", "/entrypoint.sh"] + env: + PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + metadata: + created: YYYY-MM-DD HH:MM:SS +0000 UTC + kernel: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + media-type: application/vnd.oci.image.layer.v1.tar + annotations: + org.unikraft.kernel.image: /unikraft/bin/kernel + size: X.XXXMiB + initrd: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + media-type: application/vnd.oci.image.layer.v1.tar + annotations: + org.unikraft.kernel.initrd: /unikraft/bin/initrd + size: X.XXXMiB + +$ unikraft image ls test/busybox-e2e:$UNIQ_IMAGE -okv + +stdout: + ref: test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + +$ unikraft run --name test-$UNIQ_INST --metro test --output quiet --image test/busybox-e2e:$UNIQ_IMAGE + +stdout: + test/test- + +$ unikraft instance wait --until state==stopped --timeout 10s test-$UNIQ_INST + +stdout: + metro: test + name: test- + uuid: 12345678-1234-1234-1234-123456789abc + state: stopped + image: test/busybox-e2e + resources: + memory: X.XXXMiB + vcpus: 1 + networks: + - uuid: 12345678-1234-1234-1234-123456789abc + private-ip: 10.X.X.X + mac: aa:bb:cc:dd:ee:ff + timestamps: + created: RELATIVE_TIME + stop: + reason: app exit + exit-code: 0 + +$ unikraft instance logs test-$UNIQ_INST + +stdout: + test- ┏ [ 0.000000] RDSEED32 is broken. Disabling the corresponding CPUID bit. + test- │ == BEGIN /etc/unikraft-e2e == + test- │ unikraft-e2e + test- │ == END /etc/unikraft-e2e == + test- │ == BEGIN ls /etc/unikraft-e2e == + test- │ /etc/unikraft-e2e + test- │ == END ls /etc/unikraft-e2e == + test- │ == BEGIN status == + test- │ UNIKRAFT_E2E_OK + test- │ == END status == + test- │ Application exited with 0x0 (exit code: 0) + test- │ [ 0.000000] reboot: Restarting system + +$ unikraft instance delete test-$UNIQ_INST + +stdout: + test- + +$ unikraft image delete test/busybox-e2e:$UNIQ_IMAGE + +stdout: + unikraft.io/test/busybox-e2e: + +$ unikraft image inspect test/busybox-e2e:$UNIQ_IMAGE + +stderr: + │ + │ error: + │ failed to resolve image "unikraft.io/test/busybox-e2e:": unikraft.io/test/busybox-e2e:: not found + │ + +exit code: 1 + +$ unikraft image ls test/busybox-e2e:$UNIQ_IMAGE -okv + +stderr: + │ + │ error: + │ references not found: [unikraft.io/test/busybox-e2e:] + │ + +exit code: 1 diff --git a/cmd/unikraft/testdata/TestGolden/build/busybox/erofs b/cmd/unikraft/testdata/TestGolden/build/busybox/erofs deleted file mode 100644 index d9a348ae..00000000 --- a/cmd/unikraft/testdata/TestGolden/build/busybox/erofs +++ /dev/null @@ -1,51 +0,0 @@ -$ unikraft build . --output test/busybox-e2e:$UNIQ_IMAGE - -stderr: - │ found buildkit addr= version=vX.Y.Z - -$ unikraft run --name test-$UNIQ_INST --metro test --output quiet --image test/busybox-e2e:$UNIQ_IMAGE - -stdout: - test/test- - -$ unikraft instance wait --until state==stopped --timeout 10s test-$UNIQ_INST - -stdout: - metro: test - name: test- - uuid: 12345678-1234-1234-1234-123456789abc - state: stopped - image: test/busybox-e2e - resources: - memory: 128MiB - vcpus: 1 - networks: - - uuid: 12345678-1234-1234-1234-123456789abc - private-ip: 10.X.X.X - mac: aa:bb:cc:dd:ee:ff - timestamps: - created: RELATIVE_TIME - stop: - reason: app exit - exit-code: 0 - -$ unikraft instance logs test-$UNIQ_INST - -stdout: - test- ┏ [ 0.000000] RDSEED32 is broken. Disabling the corresponding CPUID bit. - test- │ == BEGIN /etc/unikraft-e2e == - test- │ unikraft-e2e - test- │ == END /etc/unikraft-e2e == - test- │ == BEGIN ls /etc/unikraft-e2e == - test- │ /etc/unikraft-e2e - test- │ == END ls /etc/unikraft-e2e == - test- │ == BEGIN status == - test- │ UNIKRAFT_E2E_OK - test- │ == END status == - test- │ Application exited with 0x0 (exit code: 0) - test- │ [ 0.000000] reboot: Restarting system - -$ unikraft instance delete test-$UNIQ_INST - -stdout: - test- diff --git a/cmd/unikraft/testdata/TestGolden/build/busybox/erofs/direct-push b/cmd/unikraft/testdata/TestGolden/build/busybox/erofs/direct-push new file mode 100644 index 00000000..26bfc560 --- /dev/null +++ b/cmd/unikraft/testdata/TestGolden/build/busybox/erofs/direct-push @@ -0,0 +1,103 @@ +$ unikraft build . --output index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stderr: + │ found buildkit addr= version=vX.Y.Z + +$ unikraft image inspect index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stdout: + ref: index.unikraft.test/test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + config: + platform: kraftcloud/x86_64 + cmd: ["sh", "/entrypoint.sh"] + env: + PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + metadata: + created: YYYY-MM-DD HH:MM:SS +0000 UTC + kernel: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + media-type: application/vnd.oci.image.layer.v1.tar + annotations: + org.unikraft.kernel.image: /unikraft/bin/kernel + size: X.XXXMiB + initrd: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + media-type: application/vnd.oci.image.layer.v1.tar + annotations: + org.unikraft.kernel.initrd: /unikraft/bin/initrd + size: X.XXXMiB + +$ unikraft image ls index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE -okv + +stdout: + ref: index.unikraft.test/test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + +$ unikraft run --name test-$UNIQ_INST --metro test --output quiet --image index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stdout: + test/test- + +$ unikraft instance wait --until state==stopped --timeout 10s test-$UNIQ_INST + +stdout: + metro: test + name: test- + uuid: 12345678-1234-1234-1234-123456789abc + state: stopped + image: index.unikraft.test/test/busybox-e2e + resources: + memory: X.XXXMiB + vcpus: 1 + networks: + - uuid: 12345678-1234-1234-1234-123456789abc + private-ip: 10.X.X.X + mac: aa:bb:cc:dd:ee:ff + timestamps: + created: RELATIVE_TIME + stop: + reason: app exit + exit-code: 0 + +$ unikraft instance logs test-$UNIQ_INST + +stdout: + test- ┏ [ 0.000000] RDSEED32 is broken. Disabling the corresponding CPUID bit. + test- │ == BEGIN /etc/unikraft-e2e == + test- │ unikraft-e2e + test- │ == END /etc/unikraft-e2e == + test- │ == BEGIN ls /etc/unikraft-e2e == + test- │ /etc/unikraft-e2e + test- │ == END ls /etc/unikraft-e2e == + test- │ == BEGIN status == + test- │ UNIKRAFT_E2E_OK + test- │ == END status == + test- │ Application exited with 0x0 (exit code: 0) + test- │ [ 0.000000] reboot: Restarting system + +$ unikraft instance delete test-$UNIQ_INST + +stdout: + test- + +$ unikraft image delete index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stdout: + index.unikraft.test/test/busybox-e2e: + +$ unikraft image inspect index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE + +stderr: + │ + │ error: + │ failed to resolve image "index.unikraft.test/test/busybox-e2e:": index.unikraft.test/test/busybox-e2e:: not found + │ + +exit code: 1 + +$ unikraft image ls index.unikraft.test/test/busybox-e2e:$UNIQ_IMAGE -okv + +stdout: + ref: index.unikraft.test/test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 diff --git a/cmd/unikraft/testdata/TestGolden/build/busybox/erofs/registry b/cmd/unikraft/testdata/TestGolden/build/busybox/erofs/registry new file mode 100644 index 00000000..6bcb3560 --- /dev/null +++ b/cmd/unikraft/testdata/TestGolden/build/busybox/erofs/registry @@ -0,0 +1,107 @@ +$ unikraft build . --output test/busybox-e2e:$UNIQ_IMAGE + +stderr: + │ found buildkit addr= version=vX.Y.Z + +$ unikraft image inspect test/busybox-e2e:$UNIQ_IMAGE + +stdout: + ref: test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + config: + platform: kraftcloud/x86_64 + cmd: ["sh", "/entrypoint.sh"] + env: + PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + metadata: + created: YYYY-MM-DD HH:MM:SS +0000 UTC + kernel: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + media-type: application/vnd.oci.image.layer.v1.tar + annotations: + org.unikraft.kernel.image: /unikraft/bin/kernel + size: X.XXXMiB + initrd: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + media-type: application/vnd.oci.image.layer.v1.tar + annotations: + org.unikraft.kernel.initrd: /unikraft/bin/initrd + size: X.XXXMiB + +$ unikraft image ls test/busybox-e2e:$UNIQ_IMAGE -okv + +stdout: + ref: test/busybox-e2e: + digest: sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 + +$ unikraft run --name test-$UNIQ_INST --metro test --output quiet --image test/busybox-e2e:$UNIQ_IMAGE + +stdout: + test/test- + +$ unikraft instance wait --until state==stopped --timeout 10s test-$UNIQ_INST + +stdout: + metro: test + name: test- + uuid: 12345678-1234-1234-1234-123456789abc + state: stopped + image: test/busybox-e2e + resources: + memory: X.XXXMiB + vcpus: 1 + networks: + - uuid: 12345678-1234-1234-1234-123456789abc + private-ip: 10.X.X.X + mac: aa:bb:cc:dd:ee:ff + timestamps: + created: RELATIVE_TIME + stop: + reason: app exit + exit-code: 0 + +$ unikraft instance logs test-$UNIQ_INST + +stdout: + test- ┏ [ 0.000000] RDSEED32 is broken. Disabling the corresponding CPUID bit. + test- │ == BEGIN /etc/unikraft-e2e == + test- │ unikraft-e2e + test- │ == END /etc/unikraft-e2e == + test- │ == BEGIN ls /etc/unikraft-e2e == + test- │ /etc/unikraft-e2e + test- │ == END ls /etc/unikraft-e2e == + test- │ == BEGIN status == + test- │ UNIKRAFT_E2E_OK + test- │ == END status == + test- │ Application exited with 0x0 (exit code: 0) + test- │ [ 0.000000] reboot: Restarting system + +$ unikraft instance delete test-$UNIQ_INST + +stdout: + test- + +$ unikraft image delete test/busybox-e2e:$UNIQ_IMAGE + +stdout: + unikraft.io/test/busybox-e2e: + +$ unikraft image inspect test/busybox-e2e:$UNIQ_IMAGE + +stderr: + │ + │ error: + │ failed to resolve image "unikraft.io/test/busybox-e2e:": unikraft.io/test/busybox-e2e:: not found + │ + +exit code: 1 + +$ unikraft image ls test/busybox-e2e:$UNIQ_IMAGE -okv + +stderr: + │ + │ error: + │ references not found: [unikraft.io/test/busybox-e2e:] + │ + +exit code: 1