diff --git a/go.mod b/go.mod index a5af45a..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.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 diff --git a/go.sum b/go.sum index c8816c8..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.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= 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/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 64eb920..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{ @@ -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, @@ -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) } } @@ -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 @@ -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 {