From 9f8ee40305242ba7d8cb9e387c1aa2d59adee801 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:44:53 +0000 Subject: [PATCH 1/4] CLI: Update hypeman SDK to a9a0d6c96059 and add new commands/flags Update hypeman-go SDK and add CLI coverage for missing SDK methods: New commands: - `hypeman image list` for client.Images.List() - `hypeman image get` for client.Images.Get() - `hypeman image delete` for client.Images.Delete() - `hypeman volume create` for client.Volumes.New() - `hypeman volume list` for client.Volumes.List() - `hypeman volume get` for client.Volumes.Get() - `hypeman volume delete` for client.Volumes.Delete() - `hypeman volume attach` for client.Instances.Volumes.Attach() - `hypeman volume detach` for client.Instances.Volumes.Detach() - `hypeman ingress get` for client.Ingresses.Get() - `hypeman build list` for client.Builds.List() - `hypeman build get` for client.Builds.Get() - `hypeman build cancel` for client.Builds.Cancel() New flags: - `--skip-guest-agent` on `hypeman run` for InstanceNewParams.SkipGuestAgent - `--skip-kernel-headers` on `hypeman run` for InstanceNewParams.SkipKernelHeaders Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 +- pkg/cmd/build.go | 150 ++++++++++++++++++ pkg/cmd/cmd.go | 2 + pkg/cmd/imagecmd.go | 175 +++++++++++++++++++++ pkg/cmd/ingresscmd.go | 38 +++++ pkg/cmd/run.go | 17 ++ pkg/cmd/volumecmd.go | 349 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 734 insertions(+), 3 deletions(-) create mode 100644 pkg/cmd/imagecmd.go create mode 100644 pkg/cmd/volumecmd.go diff --git a/go.mod b/go.mod index 5aafcea..a5af45a 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,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.9.6 + github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059 github.com/muesli/reflow v0.3.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 diff --git a/go.sum b/go.sum index 2e5c727..c8816c8 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,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.9.6 h1:gkKbUiTYPWVDa9GwX/xaf9+z+eiTYIj6oglPlir4Xbo= -github.com/kernel/hypeman-go v0.9.6/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= +github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059 h1:C5ixkSnUllJJSWBVAusdj8uX7FcS/eJE1TbzqGAvwQc= +github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index 15f2c32..f867c26 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -12,6 +12,7 @@ import ( "github.com/kernel/hypeman-go" "github.com/kernel/hypeman-go/option" + "github.com/tidwall/gjson" "github.com/urfave/cli/v3" ) @@ -24,6 +25,11 @@ var buildCmd = cli.Command{ The path argument specifies the build context directory containing the source code and Dockerfile. If not specified, the current directory is used. +Subcommands are available for managing builds: + hypeman build list List builds + hypeman build get Get build details + hypeman build cancel Cancel a build + Examples: # Build from current directory hypeman build @@ -48,6 +54,11 @@ Examples: Value: 600, }, }, + Commands: []*cli.Command{ + &buildListCmd, + &buildGetCmd, + &buildCancelCmd, + }, Action: handleBuild, HideHelpCommand: true, } @@ -281,3 +292,142 @@ func createSourceTarball(contextPath string) (*bytes.Buffer, error) { return buf, nil } + +var buildListCmd = cli.Command{ + Name: "list", + Usage: "List builds", + Action: handleBuildList, + HideHelpCommand: true, +} + +var buildGetCmd = cli.Command{ + Name: "get", + Usage: "Get build details", + ArgsUsage: "", + Action: handleBuildGet, + HideHelpCommand: true, +} + +var buildCancelCmd = cli.Command{ + Name: "cancel", + Usage: "Cancel a build", + ArgsUsage: "", + Action: handleBuildCancel, + HideHelpCommand: true, +} + +func handleBuildList(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) + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + if format != "auto" { + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Builds.List(ctx, opts...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "build list", obj, format, transform) + } + + builds, err := client.Builds.List(ctx, opts...) + if err != nil { + return err + } + + if len(*builds) == 0 { + fmt.Fprintln(os.Stderr, "No builds found.") + return nil + } + + table := NewTableWriter(os.Stdout, "ID", "STATUS", "IMAGE", "DURATION", "CREATED") + table.TruncOrder = []int{2, 0, 4} // IMAGE first, then ID, CREATED + for _, b := range *builds { + imageRef := b.ImageRef + if imageRef == "" { + imageRef = "-" + } + + duration := "-" + if b.DurationMs > 0 { + secs := b.DurationMs / 1000 + if secs < 60 { + duration = fmt.Sprintf("%ds", secs) + } else { + duration = fmt.Sprintf("%dm%ds", secs/60, secs%60) + } + } + + table.AddRow( + TruncateID(b.ID), + string(b.Status), + imageRef, + duration, + FormatTimeAgo(b.CreatedAt), + ) + } + table.Render() + + return nil +} + +func handleBuildGet(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("build ID required\nUsage: hypeman build get ") + } + + id := args[0] + + 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.Builds.Get(ctx, id, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "build get", obj, format, transform) +} + +func handleBuildCancel(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("build ID required\nUsage: hypeman build cancel ") + } + + id := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + err := client.Builds.Cancel(ctx, id, opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Cancelled build %s\n", id) + return nil +} diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 0087cd2..e2614ca 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -81,7 +81,9 @@ func init() { &startCmd, &standbyCmd, &restoreCmd, + &imageCmd, &ingressCmd, + &volumeCmd, &resourcesCmd, &deviceCmd, { diff --git a/pkg/cmd/imagecmd.go b/pkg/cmd/imagecmd.go new file mode 100644 index 0000000..c178ef7 --- /dev/null +++ b/pkg/cmd/imagecmd.go @@ -0,0 +1,175 @@ +package cmd + +import ( + "context" + "fmt" + "net/url" + "os" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var imageCmd = cli.Command{ + Name: "image", + Usage: "Manage images", + Commands: []*cli.Command{ + &imageListCmd, + &imageGetCmd, + &imageDeleteCmd, + }, + HideHelpCommand: true, +} + +var imageListCmd = cli.Command{ + Name: "list", + Usage: "List images", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display image names", + }, + }, + Action: handleImageList, + HideHelpCommand: true, +} + +var imageGetCmd = cli.Command{ + Name: "get", + Usage: "Get image details", + ArgsUsage: "", + Action: handleImageGet, + HideHelpCommand: true, +} + +var imageDeleteCmd = cli.Command{ + Name: "delete", + Aliases: []string{"rm"}, + Usage: "Delete an image", + ArgsUsage: "", + Action: handleImageDelete, + HideHelpCommand: true, +} + +func handleImageList(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) + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + if format != "auto" { + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Images.List(ctx, opts...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "image list", obj, format, transform) + } + + images, err := client.Images.List(ctx, opts...) + if err != nil { + return err + } + + quietMode := cmd.Bool("quiet") + + if quietMode { + for _, img := range *images { + fmt.Println(img.Name) + } + return nil + } + + if len(*images) == 0 { + fmt.Fprintln(os.Stderr, "No images found.") + return nil + } + + table := NewTableWriter(os.Stdout, "NAME", "STATUS", "DIGEST", "SIZE", "CREATED") + table.TruncOrder = []int{0, 2, 4} // NAME first, then DIGEST, CREATED + for _, img := range *images { + digest := img.Digest + if len(digest) > 19 { + digest = digest[:19] + } + + size := "-" + if img.SizeBytes > 0 { + size = formatBytes(img.SizeBytes) + } + + table.AddRow( + img.Name, + string(img.Status), + digest, + size, + FormatTimeAgo(img.CreatedAt), + ) + } + table.Render() + + return nil +} + +func handleImageGet(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("image name required\nUsage: hypeman image get ") + } + + name := args[0] + + 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.Images.Get(ctx, url.PathEscape(name), opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "image get", obj, format, transform) +} + +func handleImageDelete(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("image name required\nUsage: hypeman image delete ") + } + + name := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + err := client.Images.Delete(ctx, url.PathEscape(name), opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Deleted image %s\n", name) + return nil +} diff --git a/pkg/cmd/ingresscmd.go b/pkg/cmd/ingresscmd.go index 2e83b87..0a4cc32 100644 --- a/pkg/cmd/ingresscmd.go +++ b/pkg/cmd/ingresscmd.go @@ -18,6 +18,7 @@ var ingressCmd = cli.Command{ Commands: []*cli.Command{ &ingressCreateCmd, &ingressListCmd, + &ingressGetCmd, &ingressDeleteCmd, }, HideHelpCommand: true, @@ -76,6 +77,14 @@ var ingressListCmd = cli.Command{ HideHelpCommand: true, } +var ingressGetCmd = cli.Command{ + Name: "get", + Usage: "Get ingress details", + ArgsUsage: "", + Action: handleIngressGet, + HideHelpCommand: true, +} + var ingressDeleteCmd = cli.Command{ Name: "delete", Usage: "Delete an ingress", @@ -213,6 +222,35 @@ func handleIngressList(ctx context.Context, cmd *cli.Command) error { return nil } +func handleIngressGet(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("ingress ID required\nUsage: hypeman ingress get ") + } + + id := args[0] + + 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.Ingresses.Get(ctx, id, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "ingress get", obj, format, transform) +} + func handleIngressDelete(ctx context.Context, cmd *cli.Command) error { args := cmd.Args().Slice() if len(args) < 1 { diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index e23eac3..64eb920 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -99,6 +99,15 @@ Examples: Name: "bandwidth-up", Usage: `Upload bandwidth limit (e.g., "1Gbps", "125MB/s")`, }, + // Boot option flags + &cli.BoolFlag{ + Name: "skip-guest-agent", + Usage: "Skip guest-agent installation during boot (exec and stat APIs will not work)", + }, + &cli.BoolFlag{ + Name: "skip-kernel-headers", + Usage: "Skip kernel headers installation during boot for faster startup (DKMS will not work)", + }, }, Action: handleRun, HideHelpCommand: true, @@ -219,6 +228,14 @@ func handleRun(ctx context.Context, cmd *cli.Command) error { params.DiskIoBps = hypeman.Opt(diskIO) } + // Boot options + if cmd.IsSet("skip-guest-agent") { + params.SkipGuestAgent = hypeman.Opt(cmd.Bool("skip-guest-agent")) + } + if cmd.IsSet("skip-kernel-headers") { + params.SkipKernelHeaders = hypeman.Opt(cmd.Bool("skip-kernel-headers")) + } + fmt.Fprintf(os.Stderr, "Creating instance %s...\n", name) var opts []option.RequestOption diff --git a/pkg/cmd/volumecmd.go b/pkg/cmd/volumecmd.go new file mode 100644 index 0000000..7476778 --- /dev/null +++ b/pkg/cmd/volumecmd.go @@ -0,0 +1,349 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "github.com/kernel/hypeman-go" + "github.com/kernel/hypeman-go/option" + "github.com/tidwall/gjson" + "github.com/urfave/cli/v3" +) + +var volumeCmd = cli.Command{ + Name: "volume", + Usage: "Manage volumes", + Commands: []*cli.Command{ + &volumeCreateCmd, + &volumeListCmd, + &volumeGetCmd, + &volumeDeleteCmd, + &volumeAttachCmd, + &volumeDetachCmd, + }, + HideHelpCommand: true, +} + +var volumeCreateCmd = cli.Command{ + Name: "create", + Usage: "Create a new volume", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "name", + Usage: "Volume name", + Required: true, + }, + &cli.IntFlag{ + Name: "size", + Usage: "Size in gigabytes", + Required: true, + }, + &cli.StringFlag{ + Name: "id", + Usage: "Optional custom identifier (auto-generated if not provided)", + }, + }, + Action: handleVolumeCreate, + HideHelpCommand: true, +} + +var volumeListCmd = cli.Command{ + Name: "list", + Usage: "List volumes", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display volume IDs", + }, + }, + Action: handleVolumeList, + HideHelpCommand: true, +} + +var volumeGetCmd = cli.Command{ + Name: "get", + Usage: "Get volume details", + ArgsUsage: "", + Action: handleVolumeGet, + HideHelpCommand: true, +} + +var volumeDeleteCmd = cli.Command{ + Name: "delete", + Aliases: []string{"rm"}, + Usage: "Delete a volume", + ArgsUsage: "", + Action: handleVolumeDelete, + HideHelpCommand: true, +} + +var volumeAttachCmd = cli.Command{ + Name: "attach", + Usage: "Attach a volume to an instance", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"i"}, + Usage: "Instance ID or name", + Required: true, + }, + &cli.StringFlag{ + Name: "mount-path", + Usage: "Path where volume should be mounted in the guest", + Required: true, + }, + &cli.BoolFlag{ + Name: "readonly", + Usage: "Mount as read-only", + }, + }, + Action: handleVolumeAttach, + HideHelpCommand: true, +} + +var volumeDetachCmd = cli.Command{ + Name: "detach", + Usage: "Detach a volume from an instance", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "instance", + Aliases: []string{"i"}, + Usage: "Instance ID or name", + Required: true, + }, + }, + Action: handleVolumeDetach, + HideHelpCommand: true, +} + +func handleVolumeCreate(ctx context.Context, cmd *cli.Command) error { + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + params := hypeman.VolumeNewParams{ + Name: cmd.String("name"), + SizeGB: int64(cmd.Int("size")), + } + + if id := cmd.String("id"); id != "" { + params.ID = hypeman.Opt(id) + } + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Volumes.New(ctx, params, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + if format == "auto" || format == "" { + vol := gjson.ParseBytes(res) + fmt.Printf("%s\n", vol.Get("id").String()) + return nil + } + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "volume create", obj, format, transform) +} + +func handleVolumeList(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) + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + if format != "auto" { + var res []byte + opts = append(opts, option.WithResponseBodyInto(&res)) + _, err := client.Volumes.List(ctx, opts...) + if err != nil { + return err + } + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "volume list", obj, format, transform) + } + + volumes, err := client.Volumes.List(ctx, opts...) + if err != nil { + return err + } + + quietMode := cmd.Bool("quiet") + + if quietMode { + for _, vol := range *volumes { + fmt.Println(vol.ID) + } + return nil + } + + if len(*volumes) == 0 { + fmt.Fprintln(os.Stderr, "No volumes found.") + return nil + } + + table := NewTableWriter(os.Stdout, "ID", "NAME", "SIZE", "ATTACHMENTS", "CREATED") + table.TruncOrder = []int{0, 1, 4} // ID first, then NAME, CREATED + for _, vol := range *volumes { + attachments := fmt.Sprintf("%d", len(vol.Attachments)) + if len(vol.Attachments) == 0 { + attachments = "-" + } + + table.AddRow( + TruncateID(vol.ID), + vol.Name, + fmt.Sprintf("%d GB", vol.SizeGB), + attachments, + FormatTimeAgo(vol.CreatedAt), + ) + } + table.Render() + + return nil +} + +func handleVolumeGet(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("volume ID required\nUsage: hypeman volume get ") + } + + id := args[0] + + 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.Volumes.Get(ctx, id, opts...) + if err != nil { + return err + } + + format := cmd.Root().String("format") + transform := cmd.Root().String("transform") + + obj := gjson.ParseBytes(res) + return ShowJSON(os.Stdout, "volume get", obj, format, transform) +} + +func handleVolumeDelete(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("volume ID required\nUsage: hypeman volume delete ") + } + + id := args[0] + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + err := client.Volumes.Delete(ctx, id, opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Deleted volume %s\n", id) + return nil +} + +func handleVolumeAttach(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("volume ID required\nUsage: hypeman volume attach --instance --mount-path ") + } + + volumeID := args[0] + instanceIdentifier := cmd.String("instance") + mountPath := cmd.String("mount-path") + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + // Resolve instance + instanceID, err := ResolveInstance(ctx, &client, instanceIdentifier) + if err != nil { + return err + } + + params := hypeman.InstanceVolumeAttachParams{ + ID: instanceID, + MountPath: mountPath, + } + + if cmd.IsSet("readonly") { + params.Readonly = hypeman.Opt(cmd.Bool("readonly")) + } + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + _, err = client.Instances.Volumes.Attach(ctx, volumeID, params, opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Attached volume %s to instance %s at %s\n", volumeID, instanceIdentifier, mountPath) + return nil +} + +func handleVolumeDetach(ctx context.Context, cmd *cli.Command) error { + args := cmd.Args().Slice() + if len(args) < 1 { + return fmt.Errorf("volume ID required\nUsage: hypeman volume detach --instance ") + } + + volumeID := args[0] + instanceIdentifier := cmd.String("instance") + + client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) + + // Resolve instance + instanceID, err := ResolveInstance(ctx, &client, instanceIdentifier) + if err != nil { + return err + } + + params := hypeman.InstanceVolumeDetachParams{ + ID: instanceID, + } + + var opts []option.RequestOption + if cmd.Root().Bool("debug") { + opts = append(opts, debugMiddlewareOption) + } + + _, err = client.Instances.Volumes.Detach(ctx, volumeID, params, opts...) + if err != nil { + return err + } + + fmt.Fprintf(os.Stderr, "Detached volume %s from instance %s\n", volumeID, instanceIdentifier) + return nil +} From 4abb3c2e1e03a09386ac4d7c12ff919d1c3b98ee Mon Sep 17 00:00:00 2001 From: Steven Miller Date: Wed, 11 Feb 2026 14:03:54 -0800 Subject: [PATCH 2/4] Consistency tweaks --- pkg/cmd/build.go | 20 ++++++++++++++++++-- pkg/cmd/ingresscmd.go | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index f867c26..301f7ff 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -294,8 +294,15 @@ func createSourceTarball(contextPath string) (*bytes.Buffer, error) { } var buildListCmd = cli.Command{ - Name: "list", - Usage: "List builds", + Name: "list", + Usage: "List builds", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "quiet", + Aliases: []string{"q"}, + Usage: "Only display build IDs", + }, + }, Action: handleBuildList, HideHelpCommand: true, } @@ -343,6 +350,15 @@ func handleBuildList(ctx context.Context, cmd *cli.Command) error { return err } + quietMode := cmd.Bool("quiet") + + if quietMode { + for _, b := range *builds { + fmt.Println(b.ID) + } + return nil + } + if len(*builds) == 0 { fmt.Fprintln(os.Stderr, "No builds found.") return nil diff --git a/pkg/cmd/ingresscmd.go b/pkg/cmd/ingresscmd.go index 0a4cc32..6da8a52 100644 --- a/pkg/cmd/ingresscmd.go +++ b/pkg/cmd/ingresscmd.go @@ -271,7 +271,7 @@ func handleIngressDelete(ctx context.Context, cmd *cli.Command) error { return err } - fmt.Fprintf(os.Stderr, "Ingress %s deleted.\n", id) + fmt.Fprintf(os.Stderr, "Deleted ingress %s\n", id) return nil } From 9250a4764aa2155dbc5e424065d2559ccd5a0f96 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:44:50 +0000 Subject: [PATCH 3/4] CLI: Update hypeman SDK to b99ed488ace3 and add new flags - Updated hypeman-go to b99ed488ace35cbd4f84d1bec1f180adf3b7367f - Added --volume/-v flag on `hypeman run` for InstanceNewParams.Volumes - Added --base-image-digest, --cache-scope, --global-cache-key, --is-admin-build, --secrets flags on `hypeman build` for BuildNewParams Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 +-- pkg/cmd/build.go | 36 ++++++++++++++++++++++++++ pkg/cmd/run.go | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a5af45a..0f74570 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,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.9.7-0.20260211203915-a9a0d6c96059 + github.com/kernel/hypeman-go v0.9.8-0.20260211234143-b99ed488ace3 github.com/muesli/reflow v0.3.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 diff --git a/go.sum b/go.sum index c8816c8..9697c3f 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,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.9.7-0.20260211203915-a9a0d6c96059 h1:C5ixkSnUllJJSWBVAusdj8uX7FcS/eJE1TbzqGAvwQc= -github.com/kernel/hypeman-go v0.9.7-0.20260211203915-a9a0d6c96059/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= +github.com/kernel/hypeman-go v0.9.8-0.20260211234143-b99ed488ace3 h1:hhJTGHB6wrBoIBCxg3yhRqTZ7pDrIWwd9fsA05zWc68= +github.com/kernel/hypeman-go v0.9.8-0.20260211234143-b99ed488ace3/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/pkg/cmd/build.go b/pkg/cmd/build.go index 301f7ff..04f93ef 100644 --- a/pkg/cmd/build.go +++ b/pkg/cmd/build.go @@ -53,6 +53,26 @@ Examples: Usage: "Build timeout in seconds", Value: 600, }, + &cli.StringFlag{ + Name: "base-image-digest", + Usage: "Pinned base image digest for reproducible builds", + }, + &cli.StringFlag{ + Name: "cache-scope", + Usage: "Tenant-specific cache key prefix", + }, + &cli.StringFlag{ + Name: "global-cache-key", + Usage: `Global cache identifier (e.g., "node", "python", "ubuntu")`, + }, + &cli.StringFlag{ + Name: "is-admin-build", + Usage: `Set to "true" to grant push access to global cache (operator-only)`, + }, + &cli.StringFlag{ + Name: "secrets", + Usage: `JSON array of secret references to inject during build (e.g., '[{"id":"npm_token"}]')`, + }, }, Commands: []*cli.Command{ &buildListCmd, @@ -130,6 +150,22 @@ func handleBuild(ctx context.Context, cmd *cli.Command) error { params.Dockerfile = hypeman.Opt(dockerfileContent) } + if v := cmd.String("base-image-digest"); v != "" { + params.BaseImageDigest = hypeman.Opt(v) + } + if v := cmd.String("cache-scope"); v != "" { + params.CacheScope = hypeman.Opt(v) + } + if v := cmd.String("global-cache-key"); v != "" { + params.GlobalCacheKey = hypeman.Opt(v) + } + if v := cmd.String("is-admin-build"); v != "" { + params.IsAdminBuild = hypeman.Opt(v) + } + if v := cmd.String("secrets"); v != "" { + params.Secrets = hypeman.Opt(v) + } + // Start build build, err := client.Builds.New(ctx, params, opts...) if err != nil { diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index 64eb920..baf348c 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -108,6 +108,12 @@ Examples: Name: "skip-kernel-headers", Usage: "Skip kernel headers installation during boot for faster startup (DKMS will not work)", }, + // Volume mount flags + &cli.StringSliceFlag{ + Name: "volume", + Aliases: []string{"v"}, + Usage: `Attach volume at creation (format: volume-id:/mount/path[:ro[:overlay=SIZE]]). Can be repeated.`, + }, }, Action: handleRun, HideHelpCommand: true, @@ -236,6 +242,20 @@ func handleRun(ctx context.Context, cmd *cli.Command) error { params.SkipKernelHeaders = hypeman.Opt(cmd.Bool("skip-kernel-headers")) } + // Volume mounts + volumeSpecs := cmd.StringSlice("volume") + if len(volumeSpecs) > 0 { + var mounts []hypeman.VolumeMountParam + for _, spec := range volumeSpecs { + mount, err := parseVolumeSpec(spec) + if err != nil { + return fmt.Errorf("invalid volume spec %q: %w", spec, err) + } + mounts = append(mounts, mount) + } + params.Volumes = mounts + } + fmt.Fprintf(os.Stderr, "Creating instance %s...\n", name) var opts []option.RequestOption @@ -315,6 +335,53 @@ func waitForImageReady(ctx context.Context, client *hypeman.Client, img *hypeman } } +// parseVolumeSpec parses a volume mount specification string. +// Format: volume-id:/mount/path[:ro[:overlay=SIZE]] +// Examples: +// +// my-vol:/data +// my-vol:/data:ro +// my-vol:/data:ro:overlay=10GB +func parseVolumeSpec(spec string) (hypeman.VolumeMountParam, error) { + parts := strings.SplitN(spec, ":", 2) + if len(parts) < 2 { + return hypeman.VolumeMountParam{}, fmt.Errorf("expected format volume-id:/mount/path[:ro[:overlay=SIZE]]") + } + + volumeID := parts[0] + if volumeID == "" { + return hypeman.VolumeMountParam{}, fmt.Errorf("volume ID cannot be empty") + } + + remaining := parts[1] + // Split remaining by colon to get mount path and options + segments := strings.Split(remaining, ":") + mountPath := segments[0] + if mountPath == "" { + return hypeman.VolumeMountParam{}, fmt.Errorf("mount path cannot be empty") + } + + mount := hypeman.VolumeMountParam{ + VolumeID: volumeID, + MountPath: mountPath, + } + + // Parse optional flags + for _, seg := range segments[1:] { + switch { + case seg == "ro": + mount.Readonly = hypeman.Opt(true) + case strings.HasPrefix(seg, "overlay="): + mount.Overlay = hypeman.Opt(true) + mount.OverlaySize = hypeman.Opt(strings.TrimPrefix(seg, "overlay=")) + default: + return hypeman.VolumeMountParam{}, fmt.Errorf("unknown option %q", seg) + } + } + + return mount, nil +} + // showImageStatus prints image build status to stderr func showImageStatus(img *hypeman.Image) { switch img.Status { From f566b307fd70f89109c69a1d1646f61046716975 Mon Sep 17 00:00:00 2001 From: "kernel-internal[bot]" <260533166+kernel-internal[bot]@users.noreply.github.com> Date: Wed, 11 Feb 2026 23:48:28 +0000 Subject: [PATCH 4/4] CLI: Update hypeman SDK to 5fc15d8 and add vz hypervisor support - Updated hypeman-go to v0.9.8 (5fc15d8048158e22e2a14cf3deb093d331224f7d) - Added "vz" (Virtualization.framework) hypervisor option to `hypeman run --hypervisor` - Added explicit "vz" case to formatHypervisor in `hypeman ps` output Co-authored-by: Cursor --- go.mod | 2 +- go.sum | 4 ++-- pkg/cmd/ps.go | 2 ++ pkg/cmd/run.go | 6 ++++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 0f74570..f6c768c 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,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.9.8-0.20260211234143-b99ed488ace3 + github.com/kernel/hypeman-go v0.9.8 github.com/muesli/reflow v0.3.0 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 diff --git a/go.sum b/go.sum index 9697c3f..9ff0435 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,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.9.8-0.20260211234143-b99ed488ace3 h1:hhJTGHB6wrBoIBCxg3yhRqTZ7pDrIWwd9fsA05zWc68= -github.com/kernel/hypeman-go v0.9.8-0.20260211234143-b99ed488ace3/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= +github.com/kernel/hypeman-go v0.9.8 h1:DGx3em3Bzu/MR3mgVgu7sCe8NZxujlEUGVctnrzopXA= +github.com/kernel/hypeman-go v0.9.8/go.mod h1:guRrhyP9QW/ebUS1UcZ0uZLLJeGAAhDNzSi68U4M9hI= 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= diff --git a/pkg/cmd/ps.go b/pkg/cmd/ps.go index 832182b..2a957f2 100644 --- a/pkg/cmd/ps.go +++ b/pkg/cmd/ps.go @@ -110,6 +110,8 @@ func formatHypervisor(hv hypeman.InstanceHypervisor) string { return "ch" case hypeman.InstanceHypervisorQemu: return "qemu" + case hypeman.InstanceHypervisorVz: + return "vz" default: if hv == "" { return "ch" // default diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index baf348c..a58a2c1 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -84,7 +84,7 @@ Examples: // Hypervisor flag &cli.StringFlag{ Name: "hypervisor", - Usage: `Hypervisor to use: "cloud-hypervisor" or "qemu"`, + Usage: `Hypervisor to use: "cloud-hypervisor", "qemu", or "vz"`, }, // Resource limit flags &cli.StringFlag{ @@ -223,8 +223,10 @@ func handleRun(ctx context.Context, cmd *cli.Command) error { params.Hypervisor = hypeman.InstanceNewParamsHypervisorCloudHypervisor case "qemu": params.Hypervisor = hypeman.InstanceNewParamsHypervisorQemu + case "vz": + params.Hypervisor = hypeman.InstanceNewParamsHypervisorVz default: - return fmt.Errorf("invalid hypervisor: %s (must be 'cloud-hypervisor' or 'qemu')", hypervisor) + return fmt.Errorf("invalid hypervisor: %s (must be 'cloud-hypervisor', 'qemu', or 'vz')", hypervisor) } }