From 11e43744818abba898480da3c7d34eee795a75a1 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:13:14 +0000 Subject: [PATCH 1/4] CLI: Update Hypeman Go SDK to c2d10406ff6f89703f7cae2fb112e5067370902d Bumps github.com/kernel/hypeman-go to c2d1040 so the CLI builds against the latest generated SDK surface. A full enumeration of the 58 SDK methods in api.md and their param struct fields was compared against every command and flag in pkg/cmd/. No coverage gaps were found, so no new commands or flags were needed. Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 423ffd4..022554d 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.23.0 + github.com/kernel/hypeman-go v0.23.1-0.20260810140848-c2d10406ff6f 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 6109527..df97186 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.23.0 h1:07JJgYhApTAJFofx1iGRbiIn2O4un8Pm7L27WI4OiSM= -github.com/kernel/hypeman-go v0.23.0/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48= +github.com/kernel/hypeman-go v0.23.1-0.20260810140848-c2d10406ff6f h1:mN//PXQXKqoTrWWnSsjgqVDpcm3urzET5hJ/I8SCMeA= +github.com/kernel/hypeman-go v0.23.1-0.20260810140848-c2d10406ff6f/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= From fbc7dddc8c5d2c09aa16c2bce913b43c2a7b1fa2 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:30:03 +0000 Subject: [PATCH 2/4] CLI: Update hypeman SDK to fb25cdc43a87 and add ingress request header auth The SDK now exposes IngressRuleParam.RequestHeaderAuth, letting an ingress rule require a matching request header before proxying. Expose it on `hypeman ingress create` via --request-header-auth-header/--request-header-auth-value for the single-rule shorthand and a request-header-auth=HEADER:VALUE option in the --rule grammar. Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 +- pkg/cmd/ingresscmd.go | 94 ++++++++++++++++++++++++++++---------- pkg/cmd/ingresscmd_test.go | 52 ++++++++++++++++++++- 4 files changed, 124 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 022554d..9707a02 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.23.1-0.20260810140848-c2d10406ff6f + github.com/kernel/hypeman-go v0.23.1-0.20260810152434-fb25cdc43a87 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 df97186..c9489f7 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.23.1-0.20260810140848-c2d10406ff6f h1:mN//PXQXKqoTrWWnSsjgqVDpcm3urzET5hJ/I8SCMeA= -github.com/kernel/hypeman-go v0.23.1-0.20260810140848-c2d10406ff6f/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48= +github.com/kernel/hypeman-go v0.23.1-0.20260810152434-fb25cdc43a87 h1:Yv4tzRSOEVowrVEfuIA5WF+0Ar5MIBC0BvZakIt72+0= +github.com/kernel/hypeman-go v0.23.1-0.20260810152434-fb25cdc43a87/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/ingresscmd.go b/pkg/cmd/ingresscmd.go index 2a76098..50ef004 100644 --- a/pkg/cmd/ingresscmd.go +++ b/pkg/cmd/ingresscmd.go @@ -54,11 +54,20 @@ var ingressCreateCmd = cli.Command{ Name: "redirect-http", Usage: "Auto-create HTTP to HTTPS redirect (only applies when --tls is enabled)", }, + &cli.StringFlag{ + Name: "request-header-auth-header", + Usage: "Request header that must match before proxying (reserved authentication, cookie, host, framing, proxy, and hop-by-hop headers are not allowed)", + }, + &cli.StringFlag{ + Name: "request-header-auth-value", + Usage: "Exact header value required before proxying", + }, &cli.StringSliceFlag{ Name: "rule", - Usage: "Add a routing rule (can be repeated): hostname[:host-port]=instance:port[,tls][,redirect-http]. " + + Usage: "Add a routing rule (can be repeated): " + ingressRuleSpecFormat + ". " + "Omit the instance to target the positional . When any --rule is given, the single-rule " + - "shorthand flags (--hostname/--port/--host-port/--tls/--redirect-http) must not be used.", + "shorthand flags (--hostname/--port/--host-port/--tls/--redirect-http/--request-header-auth-header/" + + "--request-header-auth-value) must not be used.", }, &cli.StringFlag{ Name: "name", @@ -120,7 +129,7 @@ func handleIngressCreate(ctx context.Context, cmd *cli.Command) error { var rules []hypeman.IngressRuleParam var primaryHostname string if len(ruleSpecs) > 0 { - for _, flag := range []string{"hostname", "port", "host-port", "tls", "redirect-http"} { + for _, flag := range []string{"hostname", "port", "host-port", "tls", "redirect-http", "request-header-auth-header", "request-header-auth-value"} { if cmd.IsSet(flag) { return fmt.Errorf("--rule cannot be combined with --%s; provide all rules via --rule", flag) } @@ -141,20 +150,26 @@ func handleIngressCreate(ctx context.Context, cmd *cli.Command) error { if !cmd.IsSet("port") { return fmt.Errorf("--port is required (or use --rule)") } - rules = []hypeman.IngressRuleParam{ - { - Match: hypeman.IngressMatchParam{ - Hostname: hostname, - Port: hypeman.Int(int64(cmd.Int("host-port"))), - }, - Target: hypeman.IngressTargetParam{ - Instance: instance, - Port: int64(cmd.Int("port")), - }, - Tls: hypeman.Bool(cmd.Bool("tls")), - RedirectHTTP: hypeman.Bool(cmd.Bool("redirect-http")), + rule := hypeman.IngressRuleParam{ + Match: hypeman.IngressMatchParam{ + Hostname: hostname, + Port: hypeman.Int(int64(cmd.Int("host-port"))), + }, + Target: hypeman.IngressTargetParam{ + Instance: instance, + Port: int64(cmd.Int("port")), }, + Tls: hypeman.Bool(cmd.Bool("tls")), + RedirectHTTP: hypeman.Bool(cmd.Bool("redirect-http")), } + auth, err := requestHeaderAuthFromFlags(cmd.String("request-header-auth-header"), cmd.String("request-header-auth-value")) + if err != nil { + return err + } + if auth != nil { + rule.RequestHeaderAuth = *auth + } + rules = []hypeman.IngressRuleParam{rule} primaryHostname = hostname } @@ -328,13 +343,17 @@ func handleIngressDelete(ctx context.Context, cmd *cli.Command) error { return nil } -// parseIngressRuleSpec parses a routing rule specification string. -// Format: hostname[:host-port]=instance:port[,tls][,redirect-http] -// When the instance is omitted (e.g. "host:80=:8080"), fallbackInstance is used. +// ingressRuleSpecFormat documents the --rule grammar shared by the flag usage +// text and the parser error message. +const ingressRuleSpecFormat = "hostname[:host-port]=instance:port[,tls][,redirect-http][,request-header-auth=HEADER:VALUE]" + +// parseIngressRuleSpec parses a routing rule specification string in the +// ingressRuleSpecFormat grammar. When the instance is omitted (e.g. +// "host:80=:8080"), fallbackInstance is used. func parseIngressRuleSpec(spec, fallbackInstance string) (hypeman.IngressRuleParam, error) { matchPart, targetPart, ok := strings.Cut(spec, "=") if !ok { - return hypeman.IngressRuleParam{}, fmt.Errorf("expected format hostname[:host-port]=instance:port[,tls][,redirect-http]") + return hypeman.IngressRuleParam{}, fmt.Errorf("expected format %s", ingressRuleSpecFormat) } hostname, hostPortStr, hasHostPort := strings.Cut(matchPart, ":") @@ -376,13 +395,23 @@ func parseIngressRuleSpec(spec, fallbackInstance string) (hypeman.IngressRulePar } for _, opt := range targetSegments[1:] { - switch opt { - case "tls": + switch { + case opt == "": + continue + case opt == "tls": rule.Tls = hypeman.Bool(true) - case "redirect-http": + case opt == "redirect-http": rule.RedirectHTTP = hypeman.Bool(true) - case "": - continue + case strings.HasPrefix(opt, requestHeaderAuthOption+"="): + header, value, _ := strings.Cut(strings.TrimPrefix(opt, requestHeaderAuthOption+"="), ":") + auth, err := requestHeaderAuthFromFlags(header, value) + if err != nil { + return hypeman.IngressRuleParam{}, err + } + if auth == nil { + return hypeman.IngressRuleParam{}, fmt.Errorf("%s must be HEADER:VALUE", requestHeaderAuthOption) + } + rule.RequestHeaderAuth = *auth default: return hypeman.IngressRuleParam{}, fmt.Errorf("unknown option %q", opt) } @@ -391,6 +420,23 @@ func parseIngressRuleSpec(spec, fallbackInstance string) (hypeman.IngressRulePar return rule, nil } +const requestHeaderAuthOption = "request-header-auth" + +// requestHeaderAuthFromFlags builds the request header auth param, returning nil +// when neither half was supplied. Both halves are required by the API. +func requestHeaderAuthFromFlags(header, value string) (*hypeman.IngressRuleRequestHeaderAuthParam, error) { + if header == "" && value == "" { + return nil, nil + } + if header == "" { + return nil, fmt.Errorf("request header auth requires a header name") + } + if value == "" { + return nil, fmt.Errorf("request header auth requires a value for header %q", header) + } + return &hypeman.IngressRuleRequestHeaderAuthParam{Header: header, Value: value}, nil +} + // generateIngressName generates an ingress name from hostname func generateIngressName(hostname string) string { // Replace dots with dashes diff --git a/pkg/cmd/ingresscmd_test.go b/pkg/cmd/ingresscmd_test.go index e125475..8090c00 100644 --- a/pkg/cmd/ingresscmd_test.go +++ b/pkg/cmd/ingresscmd_test.go @@ -34,9 +34,33 @@ func TestParseIngressRuleSpec(t *testing.T) { assert.Equal(t, int64(8080), rule.Target.Port) }) + t.Run("parses request header auth option", func(t *testing.T) { + rule, err := parseIngressRuleSpec("api.example.com=web:8080,tls,request-header-auth=X-Ingress-Token:s3cret", "fallback") + require.NoError(t, err) + assert.Equal(t, "X-Ingress-Token", rule.RequestHeaderAuth.Header) + assert.Equal(t, "s3cret", rule.RequestHeaderAuth.Value) + }) + + t.Run("keeps colons in the request header auth value", func(t *testing.T) { + rule, err := parseIngressRuleSpec("api.example.com=web:8080,request-header-auth=X-Token:user:pass", "fallback") + require.NoError(t, err) + assert.Equal(t, "X-Token", rule.RequestHeaderAuth.Header) + assert.Equal(t, "user:pass", rule.RequestHeaderAuth.Value) + }) + + t.Run("rejects request header auth without a value", func(t *testing.T) { + _, err := parseIngressRuleSpec("api.example.com=web:8080,request-header-auth=X-Token", "fallback") + require.EqualError(t, err, `request header auth requires a value for header "X-Token"`) + }) + + t.Run("rejects empty request header auth", func(t *testing.T) { + _, err := parseIngressRuleSpec("api.example.com=web:8080,request-header-auth=", "fallback") + require.EqualError(t, err, "request-header-auth must be HEADER:VALUE") + }) + t.Run("rejects missing target separator", func(t *testing.T) { _, err := parseIngressRuleSpec("api.example.com:80", "fallback") - require.EqualError(t, err, "expected format hostname[:host-port]=instance:port[,tls][,redirect-http]") + require.EqualError(t, err, "expected format "+ingressRuleSpecFormat) }) t.Run("rejects empty hostname", func(t *testing.T) { @@ -59,3 +83,29 @@ func TestParseIngressRuleSpec(t *testing.T) { require.EqualError(t, err, `unknown option "gzip"`) }) } + +func TestRequestHeaderAuthFromFlags(t *testing.T) { + t.Run("returns nil when neither half is set", func(t *testing.T) { + auth, err := requestHeaderAuthFromFlags("", "") + require.NoError(t, err) + assert.Nil(t, auth) + }) + + t.Run("builds the param when both halves are set", func(t *testing.T) { + auth, err := requestHeaderAuthFromFlags("X-Ingress-Token", "s3cret") + require.NoError(t, err) + require.NotNil(t, auth) + assert.Equal(t, "X-Ingress-Token", auth.Header) + assert.Equal(t, "s3cret", auth.Value) + }) + + t.Run("rejects a value without a header", func(t *testing.T) { + _, err := requestHeaderAuthFromFlags("", "s3cret") + require.EqualError(t, err, "request header auth requires a header name") + }) + + t.Run("rejects a header without a value", func(t *testing.T) { + _, err := requestHeaderAuthFromFlags("X-Ingress-Token", "") + require.EqualError(t, err, `request header auth requires a value for header "X-Ingress-Token"`) + }) +} From 30f2acffc95ee2131bebc5b6c6c401682dac4d19 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:13:27 +0000 Subject: [PATCH 3/4] CLI: Update hypeman SDK to f8a5d4586ff9 and add qemu-microvm hypervisor Updates the Hypeman Go SDK to f8a5d4586ff9c812b8b5f0aa342bb0b7b47bd3dc, which adds a qemu-microvm hypervisor backend to the hypervisor enums. The CLI validates hypervisor names against hardcoded lists, so the new backend was unselectable until those lists accepted it. Accept it on `hypeman run --hypervisor`, `hypeman snapshot restore --target-hypervisor`, and `hypeman snapshot fork --target-hypervisor`, and render it in the `hypeman ps` HV column. Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 +-- pkg/cmd/ps.go | 2 ++ pkg/cmd/ps_test.go | 5 ++++ pkg/cmd/run.go | 9 ++++-- pkg/cmd/snapshotcmd.go | 12 +++++--- pkg/cmd/snapshotcmd_test.go | 59 +++++++++++++++++++++++++++++++++++++ 7 files changed, 84 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 9707a02..84706b2 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.23.1-0.20260810152434-fb25cdc43a87 + github.com/kernel/hypeman-go v0.23.1-0.20260810200659-f8a5d4586ff9 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 c9489f7..7b998c0 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.23.1-0.20260810152434-fb25cdc43a87 h1:Yv4tzRSOEVowrVEfuIA5WF+0Ar5MIBC0BvZakIt72+0= -github.com/kernel/hypeman-go v0.23.1-0.20260810152434-fb25cdc43a87/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48= +github.com/kernel/hypeman-go v0.23.1-0.20260810200659-f8a5d4586ff9 h1:nyyAJ08MoAIBKe1HyOCowIyn6U+uRS5tFSUoSv9R50c= +github.com/kernel/hypeman-go v0.23.1-0.20260810200659-f8a5d4586ff9/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/ps.go b/pkg/cmd/ps.go index 82811d0..558f02c 100644 --- a/pkg/cmd/ps.go +++ b/pkg/cmd/ps.go @@ -135,6 +135,8 @@ func formatHypervisor(hv hypeman.InstanceHypervisor) string { return "ch" case hypeman.InstanceHypervisorQemu: return "qemu" + case hypeman.InstanceHypervisorQemuMicrovm: + return "microvm" case hypeman.InstanceHypervisorFirecracker: return "fc" case hypeman.InstanceHypervisorVz: diff --git a/pkg/cmd/ps_test.go b/pkg/cmd/ps_test.go index 29cbdba..ffa94d0 100644 --- a/pkg/cmd/ps_test.go +++ b/pkg/cmd/ps_test.go @@ -60,6 +60,11 @@ func TestFormatHypervisor(t *testing.T) { hypervisor: hypeman.InstanceHypervisorQemu, expected: "qemu", }, + { + name: "qemu-microvm", + hypervisor: hypeman.InstanceHypervisorQemuMicrovm, + expected: "microvm", + }, { name: "firecracker", hypervisor: hypeman.InstanceHypervisorFirecracker, diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 0201bae..eb87336 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -42,6 +42,9 @@ Examples: # Run with QEMU hypervisor hypeman run --hypervisor qemu myimage:latest + # Run on QEMU's minimal microvm board + hypeman run --hypervisor qemu-microvm myimage:latest + # Run with bandwidth limits hypeman run --bandwidth-down 1Gbps --bandwidth-up 500Mbps myimage:latest`, Flags: []cli.Flag{ @@ -99,7 +102,7 @@ Examples: // Hypervisor flag &cli.StringFlag{ Name: "hypervisor", - Usage: `Hypervisor to use: "cloud-hypervisor", "firecracker", "qemu", or "vz"`, + Usage: `Hypervisor backend to use: "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", or "vz". qemu-microvm uses QEMU's minimal Linux amd64 board and does not support PCI devices, hotplug memory, or more than eight virtio-mmio devices`, }, // Resource limit flags &cli.StringFlag{ @@ -338,10 +341,12 @@ func handleRun(ctx context.Context, cmd *cli.Command) error { params.Hypervisor = hypeman.InstanceNewParamsHypervisorFirecracker case "qemu": params.Hypervisor = hypeman.InstanceNewParamsHypervisorQemu + case "qemu-microvm", "microvm": + params.Hypervisor = hypeman.InstanceNewParamsHypervisorQemuMicrovm case "vz": params.Hypervisor = hypeman.InstanceNewParamsHypervisorVz default: - return fmt.Errorf("invalid hypervisor: %s (must be 'cloud-hypervisor', 'firecracker', 'qemu', or 'vz')", hypervisor) + return fmt.Errorf("invalid hypervisor: %s (must be 'cloud-hypervisor', 'firecracker', 'qemu', 'qemu-microvm', or 'vz')", hypervisor) } } diff --git a/pkg/cmd/snapshotcmd.go b/pkg/cmd/snapshotcmd.go index d70dd30..bce2c83 100644 --- a/pkg/cmd/snapshotcmd.go +++ b/pkg/cmd/snapshotcmd.go @@ -71,7 +71,7 @@ var snapshotRestoreCmd = cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "target-hypervisor", - Usage: `Optional hypervisor override: "cloud-hypervisor", "firecracker", "qemu", or "vz"`, + Usage: `Optional hypervisor override: "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", or "vz"`, }, &cli.StringFlag{ Name: "target-state", @@ -136,7 +136,7 @@ var snapshotForkCmd = cli.Command{ Flags: []cli.Flag{ &cli.StringFlag{ Name: "target-hypervisor", - Usage: `Optional hypervisor override: "cloud-hypervisor", "firecracker", "qemu", or "vz"`, + Usage: `Optional hypervisor override: "cloud-hypervisor", "firecracker", "qemu", "qemu-microvm", or "vz"`, }, &cli.StringFlag{ Name: "target-state", @@ -509,10 +509,12 @@ func parseSnapshotTargetHypervisor(raw string) (hypeman.InstanceSnapshotRestoreP return hypeman.InstanceSnapshotRestoreParamsTargetHypervisorFirecracker, nil case "qemu": return hypeman.InstanceSnapshotRestoreParamsTargetHypervisorQemu, nil + case "qemu-microvm", "microvm": + return hypeman.InstanceSnapshotRestoreParamsTargetHypervisorQemuMicrovm, nil case "vz": return hypeman.InstanceSnapshotRestoreParamsTargetHypervisorVz, nil default: - return "", fmt.Errorf("invalid target hypervisor: %s (must be cloud-hypervisor, firecracker, qemu, or vz)", raw) + return "", fmt.Errorf("invalid target hypervisor: %s (must be cloud-hypervisor, firecracker, qemu, qemu-microvm, or vz)", raw) } } @@ -537,9 +539,11 @@ func parseSnapshotForkTargetHypervisor(raw string) (hypeman.SnapshotForkParamsTa return hypeman.SnapshotForkParamsTargetHypervisorFirecracker, nil case "qemu": return hypeman.SnapshotForkParamsTargetHypervisorQemu, nil + case "qemu-microvm", "microvm": + return hypeman.SnapshotForkParamsTargetHypervisorQemuMicrovm, nil case "vz": return hypeman.SnapshotForkParamsTargetHypervisorVz, nil default: - return "", fmt.Errorf("invalid target hypervisor: %s (must be cloud-hypervisor, firecracker, qemu, or vz)", raw) + return "", fmt.Errorf("invalid target hypervisor: %s (must be cloud-hypervisor, firecracker, qemu, qemu-microvm, or vz)", raw) } } diff --git a/pkg/cmd/snapshotcmd_test.go b/pkg/cmd/snapshotcmd_test.go index baf6f35..60664ed 100644 --- a/pkg/cmd/snapshotcmd_test.go +++ b/pkg/cmd/snapshotcmd_test.go @@ -3,6 +3,7 @@ package cmd import ( "testing" + "github.com/kernel/hypeman-go" "github.com/kernel/hypeman-go/shared" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,3 +27,61 @@ func TestParseSnapshotCompressionAlgorithm(t *testing.T) { require.EqualError(t, err, "invalid compression algorithm: gzip (must be 'zstd' or 'lz4')") }) } + +func TestParseSnapshotTargetHypervisor(t *testing.T) { + tests := []struct { + raw string + expected hypeman.InstanceSnapshotRestoreParamsTargetHypervisor + }{ + {"cloud-hypervisor", hypeman.InstanceSnapshotRestoreParamsTargetHypervisorCloudHypervisor}, + {"ch", hypeman.InstanceSnapshotRestoreParamsTargetHypervisorCloudHypervisor}, + {"firecracker", hypeman.InstanceSnapshotRestoreParamsTargetHypervisorFirecracker}, + {"fc", hypeman.InstanceSnapshotRestoreParamsTargetHypervisorFirecracker}, + {"qemu", hypeman.InstanceSnapshotRestoreParamsTargetHypervisorQemu}, + {"qemu-microvm", hypeman.InstanceSnapshotRestoreParamsTargetHypervisorQemuMicrovm}, + {"MicroVM", hypeman.InstanceSnapshotRestoreParamsTargetHypervisorQemuMicrovm}, + {"vz", hypeman.InstanceSnapshotRestoreParamsTargetHypervisorVz}, + } + + for _, tt := range tests { + t.Run(tt.raw, func(t *testing.T) { + hypervisor, err := parseSnapshotTargetHypervisor(tt.raw) + require.NoError(t, err) + assert.Equal(t, tt.expected, hypervisor) + }) + } + + t.Run("rejects unsupported hypervisors", func(t *testing.T) { + _, err := parseSnapshotTargetHypervisor("xen") + require.EqualError(t, err, "invalid target hypervisor: xen (must be cloud-hypervisor, firecracker, qemu, qemu-microvm, or vz)") + }) +} + +func TestParseSnapshotForkTargetHypervisor(t *testing.T) { + tests := []struct { + raw string + expected hypeman.SnapshotForkParamsTargetHypervisor + }{ + {"cloud-hypervisor", hypeman.SnapshotForkParamsTargetHypervisorCloudHypervisor}, + {"ch", hypeman.SnapshotForkParamsTargetHypervisorCloudHypervisor}, + {"firecracker", hypeman.SnapshotForkParamsTargetHypervisorFirecracker}, + {"fc", hypeman.SnapshotForkParamsTargetHypervisorFirecracker}, + {"qemu", hypeman.SnapshotForkParamsTargetHypervisorQemu}, + {"qemu-microvm", hypeman.SnapshotForkParamsTargetHypervisorQemuMicrovm}, + {"MicroVM", hypeman.SnapshotForkParamsTargetHypervisorQemuMicrovm}, + {"vz", hypeman.SnapshotForkParamsTargetHypervisorVz}, + } + + for _, tt := range tests { + t.Run(tt.raw, func(t *testing.T) { + hypervisor, err := parseSnapshotForkTargetHypervisor(tt.raw) + require.NoError(t, err) + assert.Equal(t, tt.expected, hypervisor) + }) + } + + t.Run("rejects unsupported hypervisors", func(t *testing.T) { + _, err := parseSnapshotForkTargetHypervisor("xen") + require.EqualError(t, err, "invalid target hypervisor: xen (must be cloud-hypervisor, firecracker, qemu, qemu-microvm, or vz)") + }) +} From 02aef63a34ce26be65e8f00d33db856b022c636a Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:10:26 +0000 Subject: [PATCH 4/4] CLI: Update hypeman SDK to 4761d6828471a6c40c298a74934837cb01b17aad Full enumeration of SDK methods (api.md) and param struct fields against the CLI command/flag tree found no coverage gaps, so this is a dependency bump only. Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 84706b2..710a133 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.23.1-0.20260810200659-f8a5d4586ff9 + github.com/kernel/hypeman-go v0.23.1-0.20260810220603-4761d6828471 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 7b998c0..bc20442 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.23.1-0.20260810200659-f8a5d4586ff9 h1:nyyAJ08MoAIBKe1HyOCowIyn6U+uRS5tFSUoSv9R50c= -github.com/kernel/hypeman-go v0.23.1-0.20260810200659-f8a5d4586ff9/go.mod h1:of8qI/nef2OPLzt0EMlIRbMdJHEvuc4yWG8g/ioNg48= +github.com/kernel/hypeman-go v0.23.1-0.20260810220603-4761d6828471 h1:cTNudg4k6HHBy6htfeNIGvn2r80p1DcrQ9mEBbKB2pM= +github.com/kernel/hypeman-go v0.23.1-0.20260810220603-4761d6828471/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=