From 2a140701f879c8f7d155af77236390837087cb3d Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:30:31 +0000 Subject: [PATCH 1/2] CLI: Update hypeman SDK to 913f5b9d8432 and add image pull credentials The SDK now accepts Docker-style registry credentials on ImageNewParams, so a private-registry image can be pulled without the server holding credentials for that registry. Expose them as --username/--password/--registry-token on `hypeman image create` and `hypeman pull`. `hypeman push create` already had the same three flags for the outbound push credentials, so both call sites now share one flag set and one builder. A full enumeration of the 61 SDK methods in api.md and their param struct fields against the CLI command tree found no other coverage gaps. Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 +- pkg/cmd/imagecmd.go | 6 ++- pkg/cmd/pull.go | 3 ++ pkg/cmd/pushcmd.go | 32 ++----------- pkg/cmd/registrycredentials.go | 57 ++++++++++++++++++++++ pkg/cmd/registrycredentials_test.go | 74 +++++++++++++++++++++++++++++ 7 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 pkg/cmd/registrycredentials.go create mode 100644 pkg/cmd/registrycredentials_test.go diff --git a/go.mod b/go.mod index f8f4aab..64fe2ac 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/google/go-containerregistry v0.20.7 github.com/gorilla/websocket v1.5.3 github.com/itchyny/json2yaml v0.1.4 - github.com/kernel/hypeman-go v0.24.0 + github.com/kernel/hypeman-go v0.24.1-0.20260814152312-913f5b9d8432 github.com/knadh/koanf/parsers/yaml v1.1.0 github.com/knadh/koanf/providers/env v1.1.0 github.com/knadh/koanf/providers/file v1.2.1 diff --git a/go.sum b/go.sum index abc73dd..6687176 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8= github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI= -github.com/kernel/hypeman-go v0.24.0 h1:kWssdYGVmnzVAYJcfbowieCazGxITUebrYil+BEBfag= -github.com/kernel/hypeman-go v0.24.0/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48= +github.com/kernel/hypeman-go v0.24.1-0.20260814152312-913f5b9d8432 h1:p2zzyxdjm4gEjorDwu+GIGfpRLhIb8Wv+F83uzxy1TQ= +github.com/kernel/hypeman-go v0.24.1-0.20260814152312-913f5b9d8432/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= diff --git a/pkg/cmd/imagecmd.go b/pkg/cmd/imagecmd.go index fe04aa1..7528105 100644 --- a/pkg/cmd/imagecmd.go +++ b/pkg/cmd/imagecmd.go @@ -170,6 +170,9 @@ func handleImageCreateLike(ctx context.Context, cmd *cli.Command, usageLine, out for _, malformed := range malformedTags { fmt.Fprintf(os.Stderr, "Warning: ignoring malformed tag: %s\n", malformed) } + if credentials, ok := registryCredentialsFromCommand(cmd); ok { + params.Credentials = credentials + } var opts []option.RequestOption if cmd.Root().Bool("debug") { @@ -199,7 +202,7 @@ func handleImageCreateLike(ctx context.Context, cmd *cli.Command, usageLine, out } func imageCreateFlags() []cli.Flag { - return []cli.Flag{ + flags := []cli.Flag{ &cli.StringSliceFlag{ Name: "tag", Usage: "Set image tag key-value pair (KEY=VALUE, can be repeated)", @@ -209,6 +212,7 @@ func imageCreateFlags() []cli.Flag { Usage: `Target platform as os/arch[/variant] (e.g., "linux/amd64"). Defaults to the host platform`, }, } + return append(flags, registryCredentialFlags()...) } func buildImageNewParams(name string, tagSpecs []string, platform string) (hypeman.ImageNewParams, []string) { diff --git a/pkg/cmd/pull.go b/pkg/cmd/pull.go index dd7c7f6..e1a3f1b 100644 --- a/pkg/cmd/pull.go +++ b/pkg/cmd/pull.go @@ -31,6 +31,9 @@ func handlePull(ctx context.Context, cmd *cli.Command) error { for _, malformed := range malformedTags { fmt.Fprintf(os.Stderr, "Warning: ignoring malformed tag: %s\n", malformed) } + if credentials, ok := registryCredentialsFromCommand(cmd); ok { + params.Credentials = credentials + } client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) diff --git a/pkg/cmd/pushcmd.go b/pkg/cmd/pushcmd.go index 64af10a..9f4f80c 100644 --- a/pkg/cmd/pushcmd.go +++ b/pkg/cmd/pushcmd.go @@ -26,24 +26,12 @@ Examples: # Push with credentials borrowed for this push only hypeman push create alpine:latest registry.example.com/myapp:v1 --username alice --password s3cret`, - Flags: []cli.Flag{ + Flags: append([]cli.Flag{ &cli.BoolFlag{ Name: "insecure", Usage: "Allow pushing to plain-HTTP registries", }, - &cli.StringFlag{ - Name: "username", - Usage: "Registry username", - }, - &cli.StringFlag{ - Name: "password", - Usage: "Registry password or access token", - }, - &cli.StringFlag{ - Name: "registry-token", - Usage: "Bearer token for an Authorization header", - }, - }, + }, registryCredentialFlags()...), Action: handlePushCreate, HideHelpCommand: true, } @@ -130,21 +118,7 @@ func buildPushNewParams(image, target string, insecure bool, username, password, params.CreatePushRequest.Insecure = hypeman.Opt(true) } - credentials := hypeman.PushCredentialsParam{} - haveCredentials := false - if username != "" { - credentials.Username = hypeman.Opt(username) - haveCredentials = true - } - if password != "" { - credentials.Password = hypeman.Opt(password) - haveCredentials = true - } - if registryToken != "" { - credentials.RegistryToken = hypeman.Opt(registryToken) - haveCredentials = true - } - if haveCredentials { + if credentials, ok := buildRegistryCredentials(username, password, registryToken); ok { params.CreatePushRequest.Credentials = credentials } diff --git a/pkg/cmd/registrycredentials.go b/pkg/cmd/registrycredentials.go new file mode 100644 index 0000000..dfefce2 --- /dev/null +++ b/pkg/cmd/registrycredentials.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "github.com/kernel/hypeman-go" + "github.com/urfave/cli/v3" +) + +// registryCredentialFlags are the Docker-style registry credentials that the +// server borrows for a single image pull or push request. They are shared by +// every command that talks to a remote registry on the caller's behalf. +func registryCredentialFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Usage: "Registry username", + }, + &cli.StringFlag{ + Name: "password", + Usage: "Registry password or access token", + }, + &cli.StringFlag{ + Name: "registry-token", + Usage: "Bearer token for an Authorization header", + }, + } +} + +func registryCredentialsFromCommand(cmd *cli.Command) (hypeman.PushCredentialsParam, bool) { + return buildRegistryCredentials( + cmd.String("username"), + cmd.String("password"), + cmd.String("registry-token"), + ) +} + +// buildRegistryCredentials reports false when nothing was supplied, so callers +// can leave the field unset and let the server use its own registry +// credentials. +func buildRegistryCredentials(username, password, registryToken string) (hypeman.PushCredentialsParam, bool) { + credentials := hypeman.PushCredentialsParam{} + supplied := false + + if username != "" { + credentials.Username = hypeman.Opt(username) + supplied = true + } + if password != "" { + credentials.Password = hypeman.Opt(password) + supplied = true + } + if registryToken != "" { + credentials.RegistryToken = hypeman.Opt(registryToken) + supplied = true + } + + return credentials, supplied +} diff --git a/pkg/cmd/registrycredentials_test.go b/pkg/cmd/registrycredentials_test.go new file mode 100644 index 0000000..89b0d6e --- /dev/null +++ b/pkg/cmd/registrycredentials_test.go @@ -0,0 +1,74 @@ +package cmd + +import ( + "context" + "testing" + + "github.com/kernel/hypeman-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v3" +) + +func TestBuildRegistryCredentials(t *testing.T) { + t.Run("reports nothing supplied when all values are empty", func(t *testing.T) { + credentials, supplied := buildRegistryCredentials("", "", "") + + assert.False(t, supplied) + assert.False(t, credentials.Username.Valid()) + assert.False(t, credentials.Password.Valid()) + assert.False(t, credentials.RegistryToken.Valid()) + }) + + t.Run("sets only the values that were supplied", func(t *testing.T) { + credentials, supplied := buildRegistryCredentials("", "", "token") + + require.True(t, supplied) + assert.False(t, credentials.Username.Valid()) + assert.False(t, credentials.Password.Valid()) + require.True(t, credentials.RegistryToken.Valid()) + assert.Equal(t, "token", credentials.RegistryToken.Value) + }) + + t.Run("sets every value", func(t *testing.T) { + credentials, supplied := buildRegistryCredentials("alice", "s3cret", "token") + + require.True(t, supplied) + require.True(t, credentials.Username.Valid()) + assert.Equal(t, "alice", credentials.Username.Value) + require.True(t, credentials.Password.Valid()) + assert.Equal(t, "s3cret", credentials.Password.Value) + require.True(t, credentials.RegistryToken.Valid()) + assert.Equal(t, "token", credentials.RegistryToken.Value) + }) +} + +// TestImageCreateFlagsCarryRegistryCredentials covers the wiring between the +// shared image-create flags and ImageNewParams.Credentials, which lets +// `hypeman pull` and `hypeman image create` pull from a private registry. +func TestImageCreateFlagsCarryRegistryCredentials(t *testing.T) { + var params hypeman.ImageNewParams + + command := &cli.Command{ + Name: "create", + Flags: imageCreateFlags(), + Action: func(_ context.Context, cmd *cli.Command) error { + params, _ = buildImageNewParams(cmd.Args().First(), nil, "") + if credentials, ok := registryCredentialsFromCommand(cmd); ok { + params.Credentials = credentials + } + return nil + }, + } + + require.NoError(t, command.Run(context.Background(), []string{ + "create", "--username", "alice", "--password", "s3cret", "alpine:latest", + })) + + require.Equal(t, "alpine:latest", params.Name) + require.True(t, params.Credentials.Username.Valid()) + assert.Equal(t, "alice", params.Credentials.Username.Value) + require.True(t, params.Credentials.Password.Valid()) + assert.Equal(t, "s3cret", params.Credentials.Password.Value) + assert.False(t, params.Credentials.RegistryToken.Valid()) +} From 2bd90ec4deb8b9dbd2532b5948aa94b042e31aff Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 19:04:33 +0000 Subject: [PATCH 2/2] CLI: Update hypeman SDK to 7f21c67 and add capabilities command Bump github.com/kernel/hypeman-go to 7f21c67d750f6dd66c6b6af04e88c710841f2daf, which adds the GET /capabilities resource. Expose it as `hypeman capabilities` so users can discover which runtimes and features a host actually supports instead of hard-coding hypervisor knowledge. Co-authored-by: Cursor --- README.md | 19 ++++ go.mod | 2 +- go.sum | 4 +- pkg/cmd/capabilitiescmd.go | 150 ++++++++++++++++++++++++++++++++ pkg/cmd/capabilitiescmd_test.go | 71 +++++++++++++++ pkg/cmd/cmd.go | 1 + 6 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 pkg/cmd/capabilitiescmd.go create mode 100644 pkg/cmd/capabilitiescmd_test.go diff --git a/README.md b/README.md index 4a925d3..df0817b 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,25 @@ The CLI also provides resource-based commands for more advanced usage: hypeman [resource] [command] [flags] ``` +## Host Capabilities + +Check what the server build supports on this host before relying on a runtime or feature: + +```bash +# Show server/API version, host OS/arch, runtimes, image platforms, and networking +hypeman capabilities + +# Show capabilities as JSON +hypeman capabilities --format json + +# Show only the runtimes this host supports +hypeman capabilities --transform runtimes +``` + +Each runtime is listed with an `available` flag and its own feature IDs (for example +`snapshots`, `standby`, `fork`, `gpu-passthrough`), so a runtime is only launchable when +its `available` flag is `yes`. + ## Resource Management ### Viewing Server Resources diff --git a/go.mod b/go.mod index 64fe2ac..e4cd043 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/google/go-containerregistry v0.20.7 github.com/gorilla/websocket v1.5.3 github.com/itchyny/json2yaml v0.1.4 - github.com/kernel/hypeman-go v0.24.1-0.20260814152312-913f5b9d8432 + github.com/kernel/hypeman-go v0.24.1-0.20260817185642-7f21c67d750f github.com/knadh/koanf/parsers/yaml v1.1.0 github.com/knadh/koanf/providers/env v1.1.0 github.com/knadh/koanf/providers/file v1.2.1 diff --git a/go.sum b/go.sum index 6687176..6dd60f2 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/itchyny/json2yaml v0.1.4 h1:/pErVOXGG5iTyXHi/QKR4y3uzhLjGTEmmJIy97YT+k8= github.com/itchyny/json2yaml v0.1.4/go.mod h1:6iudhBZdarpjLFRNj+clWLAkGft+9uCcjAZYXUH9eGI= -github.com/kernel/hypeman-go v0.24.1-0.20260814152312-913f5b9d8432 h1:p2zzyxdjm4gEjorDwu+GIGfpRLhIb8Wv+F83uzxy1TQ= -github.com/kernel/hypeman-go v0.24.1-0.20260814152312-913f5b9d8432/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48= +github.com/kernel/hypeman-go v0.24.1-0.20260817185642-7f21c67d750f h1:vgFyvKK4pXteI49Dd+0jTea0ZAK2/0Acy055MKu0ZXI= +github.com/kernel/hypeman-go v0.24.1-0.20260817185642-7f21c67d750f/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48= github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= diff --git a/pkg/cmd/capabilitiescmd.go b/pkg/cmd/capabilitiescmd.go new file mode 100644 index 0000000..f21af24 --- /dev/null +++ b/pkg/cmd/capabilitiescmd.go @@ -0,0 +1,150 @@ +package cmd + +import ( + "context" + "fmt" + "io" + "os" + "strings" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var capabilitiesCmd = cli.Command{ + Name: "capabilities", + Aliases: []string{"capability"}, + Usage: "Show machine-readable host capabilities", + Description: `Report server and API version, host OS/architecture, every runtime available on +this host with its per-runtime feature IDs, the configured default runtime and +whether it is available, guest networking model and host gateway, supported +image platforms, and stable server-level feature IDs. + +Runtime-derived values reflect the actual host (for example, snapshot and +standby support on macOS is gated on the host OS version), so clients can gate +behavior on capabilities without hard-coding hypervisor knowledge. + +Examples: + # Show capabilities (default table format) + hypeman capabilities + + # Show capabilities as JSON + hypeman capabilities --format json + + # Show only the runtimes this host supports + hypeman capabilities --transform runtimes`, + Action: handleCapabilities, + HideHelpCommand: true, +} + +func handleCapabilities(ctx context.Context, cmd *cli.Command) error { + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Capabilities.Get(ctx, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + if format == "auto" || format == "" { + return showCapabilities(os.Stdout, res) + } + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "capabilities", obj, format, transform) +} + +func showCapabilities(w io.Writer, data []byte) error { + obj := gjson.ParseBytes(data) + + server := obj.Get("server") + fmt.Fprintln(w, "SERVER") + fmt.Fprintf(w, " Version: %s\n", orDash(server.Get("version").String())) + fmt.Fprintf(w, " API version: %s\n", orDash(server.Get("api_version").String())) + + host := obj.Get("host") + fmt.Fprintln(w) + fmt.Fprintln(w, "HOST") + fmt.Fprintf(w, " OS: %s\n", orDash(host.Get("os").String())) + fmt.Fprintf(w, " Arch: %s\n", orDash(host.Get("arch").String())) + + defaultRuntime := obj.Get("default_runtime") + fmt.Fprintln(w) + fmt.Fprintln(w, "DEFAULT RUNTIME") + fmt.Fprintf(w, " Name: %s\n", orDash(defaultRuntime.Get("name").String())) + fmt.Fprintf(w, " Available: %s\n", yesNo(defaultRuntime.Get("available").Bool())) + + runtimes := obj.Get("runtimes") + if runtimes.IsArray() && len(runtimes.Array()) > 0 { + fmt.Fprintln(w) + fmt.Fprintln(w, "RUNTIMES") + table := NewTableWriter(w, "NAME", "AVAILABLE", "FEATURES") + table.TruncOrder = []int{2} + runtimes.ForEach(func(_, value gjson.Result) bool { + table.AddRow( + value.Get("name").String(), + yesNo(value.Get("available").Bool()), + orDash(joinStrings(value.Get("features"))), + ) + return true + }) + table.Render() + } + + images := obj.Get("images") + fmt.Fprintln(w) + fmt.Fprintln(w, "IMAGES") + fmt.Fprintf(w, " Default platform: %s\n", orDash(images.Get("default_platform").String())) + fmt.Fprintf(w, " Platforms: %s\n", orDash(joinStrings(images.Get("platforms")))) + + network := obj.Get("network") + fmt.Fprintln(w) + fmt.Fprintln(w, "NETWORK") + fmt.Fprintf(w, " Model: %s\n", orDash(network.Get("model").String())) + fmt.Fprintf(w, " Gateway: %s\n", orDash(network.Get("gateway").String())) + fmt.Fprintf(w, " Subnet: %s\n", orDash(network.Get("subnet").String())) + fmt.Fprintf(w, " Guest to guest: %s\n", yesNo(network.Get("guest_to_guest").Bool())) + + fmt.Fprintln(w) + fmt.Fprintln(w, "SERVER FEATURES") + fmt.Fprintf(w, " %s\n", orDash(joinStrings(obj.Get("features")))) + + return nil +} + +func joinStrings(arr gjson.Result) string { + if !arr.IsArray() { + return "" + } + values := make([]string, 0, len(arr.Array())) + arr.ForEach(func(_, value gjson.Result) bool { + values = append(values, value.String()) + return true + }) + return strings.Join(values, ", ") +} + +func orDash(s string) string { + if s == "" { + return "-" + } + return s +} + +func yesNo(b bool) string { + if b { + return "yes" + } + return "no" +} diff --git a/pkg/cmd/capabilitiescmd_test.go b/pkg/cmd/capabilitiescmd_test.go new file mode 100644 index 0000000..7caea94 --- /dev/null +++ b/pkg/cmd/capabilitiescmd_test.go @@ -0,0 +1,71 @@ +package cmd + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCapabilitiesCmdStructure(t *testing.T) { + assert.Equal(t, "capabilities", capabilitiesCmd.Name) + assert.Contains(t, capabilitiesCmd.Aliases, "capability") + assert.NotNil(t, capabilitiesCmd.Action) +} + +func TestShowCapabilities(t *testing.T) { + payload := []byte(`{ + "default_runtime": {"available": true, "name": "cloud-hypervisor"}, + "features": ["instances", "images", "devices"], + "host": {"arch": "amd64", "os": "linux"}, + "images": {"default_platform": "linux/amd64", "platforms": ["linux/amd64", "linux/arm64"]}, + "network": {"guest_to_guest": false, "model": "bridge", "gateway": "192.168.100.1", "subnet": "192.168.100.0/24"}, + "runtimes": [ + {"available": true, "features": ["snapshots", "standby"], "name": "cloud-hypervisor"}, + {"available": false, "features": [], "name": "qemu"} + ], + "server": {"api_version": "1.2.3", "version": "abc1234"} + }`) + + var buf bytes.Buffer + require.NoError(t, showCapabilities(&buf, payload)) + out := buf.String() + + assert.Contains(t, out, "Version: abc1234") + assert.Contains(t, out, "API version: 1.2.3") + assert.Contains(t, out, "OS: linux") + assert.Contains(t, out, "Arch: amd64") + assert.Contains(t, out, "Name: cloud-hypervisor") + assert.Contains(t, out, "cloud-hypervisor yes") + assert.Contains(t, out, "snapshots, standby") + assert.Contains(t, out, "qemu no") + assert.Contains(t, out, "Default platform: linux/amd64") + assert.Contains(t, out, "Platforms: linux/amd64, linux/arm64") + assert.Contains(t, out, "Model: bridge") + assert.Contains(t, out, "Gateway: 192.168.100.1") + assert.Contains(t, out, "Subnet: 192.168.100.0/24") + assert.Contains(t, out, "Guest to guest: no") + assert.Contains(t, out, "instances, images, devices") +} + +func TestShowCapabilitiesOmitsMissingOptionalFields(t *testing.T) { + payload := []byte(`{ + "default_runtime": {"available": false, "name": "vz"}, + "features": [], + "host": {"arch": "arm64", "os": "darwin"}, + "images": {"default_platform": "linux/arm64", "platforms": ["linux/arm64"]}, + "network": {"guest_to_guest": true, "model": "nat"}, + "runtimes": [], + "server": {"api_version": "1.2.3", "version": "unknown"} + }`) + + var buf bytes.Buffer + require.NoError(t, showCapabilities(&buf, payload)) + out := buf.String() + + assert.Contains(t, out, "Gateway: -") + assert.Contains(t, out, "Subnet: -") + assert.NotContains(t, out, "RUNTIMES") + assert.Contains(t, out, "SERVER FEATURES\n -") +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 14f266b..18284aa 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -94,6 +94,7 @@ func init() { &volumeCmd, &resourcesCmd, &healthCmd, + &capabilitiesCmd, &deviceCmd, &composeCmd, {