Skip to content
Open
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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.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=
Expand Down
150 changes: 150 additions & 0 deletions pkg/cmd/capabilitiescmd.go
Original file line number Diff line number Diff line change
@@ -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"
}
71 changes: 71 additions & 0 deletions pkg/cmd/capabilitiescmd_test.go
Original file line number Diff line number Diff line change
@@ -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 -")
}
1 change: 1 addition & 0 deletions pkg/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func init() {
&volumeCmd,
&resourcesCmd,
&healthCmd,
&capabilitiesCmd,
&deviceCmd,
&composeCmd,
{
Expand Down
6 changes: 5 additions & 1 deletion pkg/cmd/imagecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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)",
Expand All @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions pkg/cmd/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)...)

Expand Down
32 changes: 3 additions & 29 deletions pkg/cmd/pushcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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
}

Expand Down
Loading
Loading