mirror of
https://github.com/cirruslabs/orchard.git
synced 2026-09-29 19:41:15 +02:00
Make USB accessories opt-in for Tart VMs (#502)
This commit is contained in:
@@ -729,6 +729,15 @@ components:
|
||||
- "66.66.0.0/16"
|
||||
items:
|
||||
type: string
|
||||
usbAccessories:
|
||||
type: boolean
|
||||
description: |
|
||||
Whether to enable USB accessories in a Tart VM. When disabled, the worker
|
||||
passes `--no-usb-accessories` to `tart run`. Requires a Tart version that supports
|
||||
this flag. Native Mac input remains available where supported.
|
||||
Changing this setting restarts the VM and is not supported for
|
||||
suspendable VMs.
|
||||
default: false
|
||||
suspendable:
|
||||
type: boolean
|
||||
description: |
|
||||
|
||||
@@ -31,6 +31,7 @@ var nested bool
|
||||
var audio bool
|
||||
var clipboard bool
|
||||
var suspendable bool
|
||||
var usbAccessories bool
|
||||
var username string
|
||||
var password string
|
||||
var resources map[string]string
|
||||
@@ -75,6 +76,7 @@ func newCreateVMCommand() *cobra.Command {
|
||||
command.Flags().BoolVar(&nested, "nested", false, "enable nested virtualization")
|
||||
command.Flags().BoolVar(&audio, "audio", false, "enable audio pass-through to the host")
|
||||
command.Flags().BoolVar(&clipboard, "clipboard", false, "enable clipboard sharing between host and guest")
|
||||
command.Flags().BoolVar(&usbAccessories, "usb-accessories", false, "enable USB accessories in a Tart VM")
|
||||
command.Flags().BoolVar(&suspendable, "suspendable", false, "treat the VM as suspendable, "+
|
||||
"disabling certain devices for suspendability support and issuing \"tart suspend\" instead of \"tart stop\" "+
|
||||
"when VM's specification is updated, thus preserving the VM's state between specification generations")
|
||||
@@ -160,6 +162,7 @@ func runCreateVM(cmd *cobra.Command, args []string) error {
|
||||
NetSoftnetAllow: netSoftnetAllow,
|
||||
NetSoftnetBlock: netSoftnetBlock,
|
||||
Suspendable: suspendable,
|
||||
USBAccessories: usbAccessories,
|
||||
},
|
||||
NetBridged: netBridged,
|
||||
Headless: headless,
|
||||
|
||||
@@ -238,6 +238,10 @@ func (controller *Controller) updateVMSpec(ctx *gin.Context) responder.Responder
|
||||
return responder.JSON(http.StatusPreconditionFailed, NewErrorResponse("\"netSoftnet\" cannot be "+
|
||||
"toggled for suspendable VMs"))
|
||||
}
|
||||
if dbVM.Suspendable && dbVM.USBAccessories != userVM.USBAccessories {
|
||||
return responder.JSON(http.StatusPreconditionFailed, NewErrorResponse("\"usbAccessories\" cannot be "+
|
||||
"toggled for suspendable VMs"))
|
||||
}
|
||||
|
||||
// Power state-specific sanity checks
|
||||
if !userVM.PowerState.Valid() {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package tests_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cirruslabs/orchard/internal/controller"
|
||||
"github.com/cirruslabs/orchard/internal/tests/devcontroller"
|
||||
"github.com/cirruslabs/orchard/internal/worker"
|
||||
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUSBAccessoriesSpecUpdate(t *testing.T) {
|
||||
devClient, _, _ := devcontroller.StartIntegrationTestEnvironmentWithAdditionalOpts(
|
||||
t, false, []controller.Option{controller.WithSynthetic()},
|
||||
true, []worker.Option{worker.WithSynthetic()},
|
||||
)
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
usbAccessories bool
|
||||
suspendable bool
|
||||
}{
|
||||
{name: "enable"},
|
||||
{name: "disable", usbAccessories: true},
|
||||
{name: "suspendable-enable", suspendable: true},
|
||||
{name: "suspendable-disable", usbAccessories: true, suspendable: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
require.NoError(t, devClient.VMs().Create(t.Context(), &v1.VM{
|
||||
Name: test.name,
|
||||
Image: "example.com/test:latest",
|
||||
USBAccessories: test.usbAccessories,
|
||||
Suspendable: test.suspendable,
|
||||
}))
|
||||
vm, err := devClient.VMs().Get(t.Context(), test.name)
|
||||
require.NoError(t, err)
|
||||
generation := vm.Generation
|
||||
vm.USBAccessories = !vm.USBAccessories
|
||||
|
||||
updated, err := devClient.VMs().Update(t.Context(), *vm)
|
||||
if test.suspendable {
|
||||
require.ErrorContains(t, err, `"usbAccessories" cannot be toggled for suspendable VMs`)
|
||||
unchanged, getErr := devClient.VMs().Get(t.Context(), test.name)
|
||||
require.NoError(t, getErr)
|
||||
require.Equal(t, generation, unchanged.Generation)
|
||||
require.Equal(t, test.usbAccessories, unchanged.USBAccessories)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, generation+1, updated.Generation)
|
||||
require.Equal(t, !test.usbAccessories, updated.USBAccessories)
|
||||
require.False(t, updated.Suspendable)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -372,6 +372,10 @@ func (vm *VM) run(ctx context.Context, eventStreamer *client.EventStreamer) {
|
||||
runArgs = append(runArgs, "--no-clipboard")
|
||||
}
|
||||
|
||||
if !resource.USBAccessories {
|
||||
runArgs = append(runArgs, "--no-usb-accessories")
|
||||
}
|
||||
|
||||
if resource.Suspendable {
|
||||
runArgs = append(runArgs, "--suspendable")
|
||||
}
|
||||
|
||||
@@ -54,6 +54,39 @@ func TestCloneAndConfigurePreservesSuspendedVM(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunUSBAccessoriesConfiguration(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
usbAccessories bool
|
||||
suspendable bool
|
||||
wantFlags string
|
||||
}{
|
||||
{name: "default", wantFlags: "--no-usb-accessories "},
|
||||
{name: "USB accessories enabled", usbAccessories: true},
|
||||
{name: "suspendable", suspendable: true, wantFlags: "--no-usb-accessories --suspendable "},
|
||||
{name: "both", usbAccessories: true, suspendable: true, wantFlags: "--suspendable "},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
commandLog := installCloneFakeTart(t, "", "")
|
||||
vm := newCloneTestVM(v1.VM{
|
||||
Name: "test-vm",
|
||||
UID: "00112233-4455-6677-8899-aabbccddeeff",
|
||||
Audio: true,
|
||||
Clipboard: true,
|
||||
USBAccessories: test.usbAccessories,
|
||||
Suspendable: test.suspendable,
|
||||
})
|
||||
vm.ctx = t.Context()
|
||||
vm.ConditionsSet().Add(v1.ConditionTypeStopping)
|
||||
|
||||
vm.run(t.Context(), nil)
|
||||
|
||||
require.NoError(t, vm.Err())
|
||||
requireCloneCommands(t, commandLog, []string{"run " + test.wantFlags + vm.id()})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneAndConfigureConfiguresStoppedVM(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -197,7 +230,7 @@ if [ "$1" = "$ORCHARD_TEST_TART_FAILED_COMMAND" ]; then
|
||||
exit 1
|
||||
fi
|
||||
case "$1" in
|
||||
clone|set) ;;
|
||||
clone|set|run) ;;
|
||||
fqn) printf 'registry.example/source@sha256:abc\n' ;;
|
||||
get) printf '%s\n' "$ORCHARD_TEST_TART_INFO" ;;
|
||||
*) printf 'unexpected command: %s\n' "$*" >&2; exit 1 ;;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package v1_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUSBAccessoriesSpecification(t *testing.T) {
|
||||
var vm v1.VM
|
||||
require.NoError(t, json.Unmarshal([]byte(`{"runtime":"tart"}`), &vm))
|
||||
require.False(t, vm.USBAccessories)
|
||||
require.False(t, vm.Suspendable)
|
||||
require.NoError(t, vm.Validate())
|
||||
|
||||
previous := vm.VMSpec
|
||||
vm.USBAccessories = true
|
||||
require.False(t, v1.SemanticallyEqual(previous, vm.VMSpec))
|
||||
|
||||
encoded, err := json.Marshal(vm.VMSpec)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, string(encoded), `"usbAccessories":true`)
|
||||
|
||||
vm.Runtime = v1.RuntimeVetu
|
||||
require.ErrorContains(t, vm.Validate(), `does not support field "usbAccessories"`)
|
||||
vm.USBAccessories = false
|
||||
require.NoError(t, vm.Validate())
|
||||
}
|
||||
@@ -180,6 +180,9 @@ func (vm *VM) Validate() error {
|
||||
if vm.Suspendable {
|
||||
return unsupportedFieldError("suspendable")
|
||||
}
|
||||
if vm.USBAccessories {
|
||||
return unsupportedFieldError("usbAccessories")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -211,6 +214,7 @@ type VMSpec struct {
|
||||
NetSoftnetAllow []string `json:"netSoftnetAllow,omitempty"`
|
||||
NetSoftnetBlock []string `json:"netSoftnetBlock,omitempty"`
|
||||
Suspendable bool `json:"suspendable,omitempty"`
|
||||
USBAccessories bool `json:"usbAccessories,omitempty"`
|
||||
PowerState PowerState `json:"powerState,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user