@@ -288,6 +288,14 @@ func newVMsCreateCmd() *cobra.Command {
288288 if err != nil {
289289 return err
290290 }
291+ exposeFlags , err := cmd .Flags ().GetStringSlice ("expose" )
292+ if err != nil {
293+ return err
294+ }
295+ noExposeFlag , err := cmd .Flags ().GetBool ("no-expose" )
296+ if err != nil {
297+ return err
298+ }
291299
292300 // Build Overrides struct
293301 var overrides vmconfig.Overrides
@@ -311,7 +319,7 @@ func newVMsCreateCmd() *cobra.Command {
311319 }
312320 }
313321
314- // Parse port exposures from PORT or HOST_PORT:CONTAINER_PORT format
322+ // Parse port exposures from PORT or HOST_PORT:CONTAINER_PORT format (legacy flag)
315323 if len (portFlags ) > 0 {
316324 overrides .ExposePorts = make ([]vmconfig.Expose , 0 , len (portFlags ))
317325 for _ , p := range portFlags {
@@ -341,6 +349,90 @@ func newVMsCreateCmd() *cobra.Command {
341349 }
342350 }
343351
352+ // Parse Docker-style port exposures: [HOST_PORT:]CONTAINER_PORT[:PROTOCOL]
353+ // Examples: 8080:3000:tcp, 8080:3000, 3000:tcp, 3000
354+ if len (exposeFlags ) > 0 {
355+ if len (portFlags ) > 0 {
356+ return fmt .Errorf ("cannot use both --port and --expose flags together" )
357+ }
358+ overrides .ExposePorts = make ([]vmconfig.Expose , 0 , len (exposeFlags ))
359+ for _ , e := range exposeFlags {
360+ var expose vmconfig.Expose
361+ expose .Protocol = "tcp" // default protocol
362+
363+ parts := strings .Split (e , ":" )
364+ switch len (parts ) {
365+ case 1 :
366+ // Format: CONTAINER_PORT (auto-assign host port)
367+ containerPort , err := strconv .Atoi (parts [0 ])
368+ if err != nil {
369+ return fmt .Errorf ("invalid port in %q: %w" , e , err )
370+ }
371+ expose .Port = containerPort
372+ expose .HostPort = 0 // Auto-assign
373+
374+ case 2 :
375+ // Could be: HOST_PORT:CONTAINER_PORT or CONTAINER_PORT:PROTOCOL
376+ // Try to parse second part as protocol first
377+ protocol := strings .ToLower (parts [1 ])
378+ if protocol == "tcp" || protocol == "udp" {
379+ // Format: CONTAINER_PORT:PROTOCOL
380+ containerPort , err := strconv .Atoi (parts [0 ])
381+ if err != nil {
382+ return fmt .Errorf ("invalid port in %q: %w" , e , err )
383+ }
384+ expose .Port = containerPort
385+ expose .Protocol = protocol
386+ expose .HostPort = 0 // Auto-assign
387+ } else {
388+ // Format: HOST_PORT:CONTAINER_PORT
389+ hostPort , err := strconv .Atoi (parts [0 ])
390+ if err != nil {
391+ return fmt .Errorf ("invalid host port in %q: %w" , e , err )
392+ }
393+ containerPort , err := strconv .Atoi (parts [1 ])
394+ if err != nil {
395+ return fmt .Errorf ("invalid container port in %q: %w" , e , err )
396+ }
397+ expose .HostPort = hostPort
398+ expose .Port = containerPort
399+ }
400+
401+ case 3 :
402+ // Format: HOST_PORT:CONTAINER_PORT:PROTOCOL
403+ hostPort , err := strconv .Atoi (parts [0 ])
404+ if err != nil {
405+ return fmt .Errorf ("invalid host port in %q: %w" , e , err )
406+ }
407+ containerPort , err := strconv .Atoi (parts [1 ])
408+ if err != nil {
409+ return fmt .Errorf ("invalid container port in %q: %w" , e , err )
410+ }
411+ protocol := strings .ToLower (parts [2 ])
412+ if protocol != "tcp" && protocol != "udp" {
413+ return fmt .Errorf ("invalid protocol in %q: must be tcp or udp" , e )
414+ }
415+ expose .HostPort = hostPort
416+ expose .Port = containerPort
417+ expose .Protocol = protocol
418+
419+ default :
420+ return fmt .Errorf ("invalid expose format %q: expected [HOST_PORT:]CONTAINER_PORT[:PROTOCOL]" , e )
421+ }
422+
423+ overrides .ExposePorts = append (overrides .ExposePorts , expose )
424+ }
425+ }
426+
427+ // Handle --no-expose flag: explicitly disable all port exposure (secure by default)
428+ if noExposeFlag {
429+ if len (portFlags ) > 0 || len (exposeFlags ) > 0 {
430+ return fmt .Errorf ("cannot use --no-expose with --port or --expose flags" )
431+ }
432+ // Set to empty slice to override manifest ports
433+ overrides .ExposePorts = []vmconfig.Expose {}
434+ }
435+
344436 req := client.CreateVMRequest {
345437 Name : args [0 ],
346438 Image : imageName ,
@@ -439,7 +531,9 @@ func newVMsCreateCmd() *cobra.Command {
439531 cmd .Flags ().Int ("cpu" , 0 , "Number of virtual CPU cores (overrides manifest default)" )
440532 cmd .Flags ().Int ("memory" , 0 , "Memory in MB (overrides manifest default)" )
441533 cmd .Flags ().StringSlice ("env" , nil , "Environment variables in KEY=VALUE format (repeatable, overrides manifest defaults)" )
442- cmd .Flags ().StringSlice ("port" , nil , "Expose ports in format PORT or HOST_PORT:CONTAINER_PORT (repeatable)" )
534+ cmd .Flags ().StringSlice ("port" , nil , "Expose ports in format PORT or HOST_PORT:CONTAINER_PORT (repeatable, legacy)" )
535+ cmd .Flags ().StringSlice ("expose" , nil , "Expose ports in Docker format: [HOST_PORT:]CONTAINER_PORT[:PROTOCOL] (repeatable)" )
536+ cmd .Flags ().Bool ("no-expose" , false , "Disable all port exposure (overrides manifest defaults for secure-by-default)" )
443537 cmd .Flags ().String ("kernel-cmdline" , "" , "Additional kernel cmdline parameters" )
444538 cmd .Flags ().String ("kernel" , "" , "Override kernel image path (vmlinux)" )
445539 cmd .Flags ().String ("initramfs" , "" , "Override initramfs image path (.cpio.gz)" )
0 commit comments