@@ -108,6 +108,12 @@ Examples:
108108 Name : "skip-kernel-headers" ,
109109 Usage : "Skip kernel headers installation during boot for faster startup (DKMS will not work)" ,
110110 },
111+ // Volume mount flags
112+ & cli.StringSliceFlag {
113+ Name : "volume" ,
114+ Aliases : []string {"v" },
115+ Usage : `Attach volume at creation (format: volume-id:/mount/path[:ro[:overlay=SIZE]]). Can be repeated.` ,
116+ },
111117 },
112118 Action : handleRun ,
113119 HideHelpCommand : true ,
@@ -236,6 +242,20 @@ func handleRun(ctx context.Context, cmd *cli.Command) error {
236242 params .SkipKernelHeaders = hypeman .Opt (cmd .Bool ("skip-kernel-headers" ))
237243 }
238244
245+ // Volume mounts
246+ volumeSpecs := cmd .StringSlice ("volume" )
247+ if len (volumeSpecs ) > 0 {
248+ var mounts []hypeman.VolumeMountParam
249+ for _ , spec := range volumeSpecs {
250+ mount , err := parseVolumeSpec (spec )
251+ if err != nil {
252+ return fmt .Errorf ("invalid volume spec %q: %w" , spec , err )
253+ }
254+ mounts = append (mounts , mount )
255+ }
256+ params .Volumes = mounts
257+ }
258+
239259 fmt .Fprintf (os .Stderr , "Creating instance %s...\n " , name )
240260
241261 var opts []option.RequestOption
@@ -315,6 +335,53 @@ func waitForImageReady(ctx context.Context, client *hypeman.Client, img *hypeman
315335 }
316336}
317337
338+ // parseVolumeSpec parses a volume mount specification string.
339+ // Format: volume-id:/mount/path[:ro[:overlay=SIZE]]
340+ // Examples:
341+ //
342+ // my-vol:/data
343+ // my-vol:/data:ro
344+ // my-vol:/data:ro:overlay=10GB
345+ func parseVolumeSpec (spec string ) (hypeman.VolumeMountParam , error ) {
346+ parts := strings .SplitN (spec , ":" , 2 )
347+ if len (parts ) < 2 {
348+ return hypeman.VolumeMountParam {}, fmt .Errorf ("expected format volume-id:/mount/path[:ro[:overlay=SIZE]]" )
349+ }
350+
351+ volumeID := parts [0 ]
352+ if volumeID == "" {
353+ return hypeman.VolumeMountParam {}, fmt .Errorf ("volume ID cannot be empty" )
354+ }
355+
356+ remaining := parts [1 ]
357+ // Split remaining by colon to get mount path and options
358+ segments := strings .Split (remaining , ":" )
359+ mountPath := segments [0 ]
360+ if mountPath == "" {
361+ return hypeman.VolumeMountParam {}, fmt .Errorf ("mount path cannot be empty" )
362+ }
363+
364+ mount := hypeman.VolumeMountParam {
365+ VolumeID : volumeID ,
366+ MountPath : mountPath ,
367+ }
368+
369+ // Parse optional flags
370+ for _ , seg := range segments [1 :] {
371+ switch {
372+ case seg == "ro" :
373+ mount .Readonly = hypeman .Opt (true )
374+ case strings .HasPrefix (seg , "overlay=" ):
375+ mount .Overlay = hypeman .Opt (true )
376+ mount .OverlaySize = hypeman .Opt (strings .TrimPrefix (seg , "overlay=" ))
377+ default :
378+ return hypeman.VolumeMountParam {}, fmt .Errorf ("unknown option %q" , seg )
379+ }
380+ }
381+
382+ return mount , nil
383+ }
384+
318385// showImageStatus prints image build status to stderr
319386func showImageStatus (img * hypeman.Image ) {
320387 switch img .Status {
0 commit comments