From 3cfa2445500f45fd950ea7a664036b940b057723 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Tue, 17 Mar 2026 19:46:00 +0100 Subject: [PATCH] create vm: introduce --{os,arch,runtime} command-line arguments (#422) * create vm: introduce --{os,arch,runtime} command-line arguments * v1.VM: prevent unsupported fields for "vetu" runtime --- internal/command/create/vm.go | 35 ++++++++++++++++++++++++++++++++-- internal/controller/api_vms.go | 8 ++++++++ pkg/resource/v1/v1.go | 27 ++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/internal/command/create/vm.go b/internal/command/create/vm.go index 353f86d..2311af7 100644 --- a/internal/command/create/vm.go +++ b/internal/command/create/vm.go @@ -16,6 +16,9 @@ import ( var ErrVMFailed = errors.New("failed to create VM") var image string +var vmOSRaw string +var vmArchRaw string +var vmRuntimeRaw string var cpu uint64 var memory uint64 var diskSize uint64 @@ -45,6 +48,14 @@ func newCreateVMCommand() *cobra.Command { } command.Flags().StringVar(&image, "image", imageconstant.DefaultMacosImage, "image to use") + command.Flags().StringVar(&vmOSRaw, "os", string(v1.OSDarwin), fmt.Sprintf("operating system of this "+ + "VM: %q or %q; set to \"linux\" to work around the Apple's limitation of 2 macOS VMs per host when using "+ + "\"tart\" runtime", v1.OSDarwin, v1.OSLinux)) + command.Flags().StringVar(&vmArchRaw, "arch", string(v1.ArchitectureARM64), fmt.Sprintf("architecture "+ + "of this VM: %q or %q; ensures the VM is scheduled on an architecture-compatible machine in mixed-architecture "+ + "clusters", v1.ArchitectureARM64, v1.ArchitectureAMD64)) + command.Flags().StringVar(&vmRuntimeRaw, "runtime", string(v1.RuntimeTart), fmt.Sprintf("runtime to use "+ + "for this VM: %q or %q; ensures the VM is scheduled on a runtime-compatible node", v1.RuntimeTart, v1.RuntimeVetu)) command.Flags().Uint64Var(&cpu, "cpu", 4, "number of CPUs to use") command.Flags().Uint64Var(&memory, "memory", 8*1024, "megabytes of memory to use") command.Flags().Uint64Var(&diskSize, "disk-size", 0, "resize the VMs disk to the specified size in GB "+ @@ -100,6 +111,21 @@ func runCreateVM(cmd *cobra.Command, args []string) error { // Convert arguments var hostDirs []v1.HostDir + vmOS, err := v1.NewOSFromString(vmOSRaw) + if err != nil { + return fmt.Errorf("%w: %v", ErrVMFailed, err) + } + + vmArch, err := v1.NewArchitectureFromString(vmArchRaw) + if err != nil { + return fmt.Errorf("%w: %v", ErrVMFailed, err) + } + + vmRuntime, err := v1.NewRuntimeFromString(vmRuntimeRaw) + if err != nil { + return fmt.Errorf("%w: %v", ErrVMFailed, err) + } + for _, hostDirRaw := range hostDirsRaw { hostDir, err := v1.NewHostDirFromString(hostDirRaw) if err != nil { @@ -118,6 +144,9 @@ func runCreateVM(cmd *cobra.Command, args []string) error { Memory: memory, DiskSize: diskSize, VMSpec: v1.VMSpec{ + OS: vmOS, + Arch: vmArch, + Runtime: vmRuntime, NetSoftnetDeprecated: netSoftnet, NetSoftnet: netSoftnet, NetSoftnetAllow: netSoftnetAllow, @@ -134,9 +163,11 @@ func runCreateVM(cmd *cobra.Command, args []string) error { HostDirs: hostDirs, } - // Convert resources - var err error + if err := vm.Validate(); err != nil { + return fmt.Errorf("%w: %v", ErrVMFailed, err) + } + // Convert resources vm.Resources, err = v1.NewResourcesFromStringToString(resources) if err != nil { return fmt.Errorf("%w: %v", ErrVMFailed, err) diff --git a/internal/controller/api_vms.go b/internal/controller/api_vms.go index 3bdf281..593d4db 100644 --- a/internal/controller/api_vms.go +++ b/internal/controller/api_vms.go @@ -73,6 +73,10 @@ func (controller *Controller) createVM(ctx *gin.Context) responder.Responder { vm.Runtime = v1.RuntimeTart } + if err := vm.Validate(); err != nil { + return responder.JSON(http.StatusPreconditionFailed, NewErrorResponse("%v", err)) + } + // Softnet-specific logic: automatically enable Softnet when NetSoftnetAllow or NetSoftnetBlock are set // and propagate deprecated and non-deprecated boolean fields into each other if vm.NetSoftnetDeprecated || vm.NetSoftnet || len(vm.NetSoftnetAllow) != 0 || len(vm.NetSoftnetBlock) != 0 { @@ -173,6 +177,10 @@ func (controller *Controller) updateVMSpec(ctx *gin.Context) responder.Responder "and \"runtime\" fields cannot be modified")) } + if err := userVM.Validate(); err != nil { + return responder.JSON(http.StatusPreconditionFailed, NewErrorResponse("%v", err)) + } + // Softnet-specific logic: automatically enable Softnet when NetSoftnetAllow or NetSoftnetBlock are set // and propagate deprecated and non-deprecated boolean fields into each other if userVM.NetSoftnetDeprecated || userVM.NetSoftnet || len(userVM.NetSoftnetAllow) != 0 || len(userVM.NetSoftnetBlock) != 0 { diff --git a/pkg/resource/v1/v1.go b/pkg/resource/v1/v1.go index 7feabc0..518c5a6 100644 --- a/pkg/resource/v1/v1.go +++ b/pkg/resource/v1/v1.go @@ -137,6 +137,33 @@ func (vm *VM) IsScheduled() bool { } } +func (vm *VM) Validate() error { + unsupportedFieldError := func(field string) error { + return fmt.Errorf("runtime %q does not support field %q", vm.Runtime, field) + } + + switch vm.Runtime { + case RuntimeVetu: + if vm.NetSoftnetDeprecated || vm.NetSoftnet { + return unsupportedFieldError("netSoftnet") + } + if len(vm.NetSoftnetAllow) != 0 { + return unsupportedFieldError("netSoftnetAllow") + } + if len(vm.NetSoftnetBlock) != 0 { + return unsupportedFieldError("netSoftnetBlock") + } + if len(vm.HostDirs) != 0 { + return unsupportedFieldError("hostDirs") + } + if vm.Suspendable { + return unsupportedFieldError("suspendable") + } + } + + return nil +} + type VMSpec struct { // OS defines the operating system used by a VM. //