Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
github.com/muesli/reflow v0.3.0
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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 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=
Expand Down
36 changes: 36 additions & 0 deletions pkg/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions pkg/cmd/ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 71 additions & 2 deletions pkg/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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,
Expand Down Expand Up @@ -217,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)
}
}

Expand All @@ -236,6 +244,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
Expand Down Expand Up @@ -315,6 +337,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 {
Expand Down