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"
|
- "66.66.0.0/16"
|
||||||
items:
|
items:
|
||||||
type: string
|
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:
|
suspendable:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ var nested bool
|
|||||||
var audio bool
|
var audio bool
|
||||||
var clipboard bool
|
var clipboard bool
|
||||||
var suspendable bool
|
var suspendable bool
|
||||||
|
var usbAccessories bool
|
||||||
var username string
|
var username string
|
||||||
var password string
|
var password string
|
||||||
var resources map[string]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(&nested, "nested", false, "enable nested virtualization")
|
||||||
command.Flags().BoolVar(&audio, "audio", false, "enable audio pass-through to the host")
|
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(&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, "+
|
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\" "+
|
"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")
|
"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,
|
NetSoftnetAllow: netSoftnetAllow,
|
||||||
NetSoftnetBlock: netSoftnetBlock,
|
NetSoftnetBlock: netSoftnetBlock,
|
||||||
Suspendable: suspendable,
|
Suspendable: suspendable,
|
||||||
|
USBAccessories: usbAccessories,
|
||||||
},
|
},
|
||||||
NetBridged: netBridged,
|
NetBridged: netBridged,
|
||||||
Headless: headless,
|
Headless: headless,
|
||||||
|
|||||||
@@ -238,6 +238,10 @@ func (controller *Controller) updateVMSpec(ctx *gin.Context) responder.Responder
|
|||||||
return responder.JSON(http.StatusPreconditionFailed, NewErrorResponse("\"netSoftnet\" cannot be "+
|
return responder.JSON(http.StatusPreconditionFailed, NewErrorResponse("\"netSoftnet\" cannot be "+
|
||||||
"toggled for suspendable VMs"))
|
"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
|
// Power state-specific sanity checks
|
||||||
if !userVM.PowerState.Valid() {
|
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")
|
runArgs = append(runArgs, "--no-clipboard")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !resource.USBAccessories {
|
||||||
|
runArgs = append(runArgs, "--no-usb-accessories")
|
||||||
|
}
|
||||||
|
|
||||||
if resource.Suspendable {
|
if resource.Suspendable {
|
||||||
runArgs = append(runArgs, "--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) {
|
func TestCloneAndConfigureConfiguresStoppedVM(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -197,7 +230,7 @@ if [ "$1" = "$ORCHARD_TEST_TART_FAILED_COMMAND" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
case "$1" in
|
case "$1" in
|
||||||
clone|set) ;;
|
clone|set|run) ;;
|
||||||
fqn) printf 'registry.example/source@sha256:abc\n' ;;
|
fqn) printf 'registry.example/source@sha256:abc\n' ;;
|
||||||
get) printf '%s\n' "$ORCHARD_TEST_TART_INFO" ;;
|
get) printf '%s\n' "$ORCHARD_TEST_TART_INFO" ;;
|
||||||
*) printf 'unexpected command: %s\n' "$*" >&2; exit 1 ;;
|
*) 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 {
|
if vm.Suspendable {
|
||||||
return unsupportedFieldError("suspendable")
|
return unsupportedFieldError("suspendable")
|
||||||
}
|
}
|
||||||
|
if vm.USBAccessories {
|
||||||
|
return unsupportedFieldError("usbAccessories")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -211,6 +214,7 @@ type VMSpec struct {
|
|||||||
NetSoftnetAllow []string `json:"netSoftnetAllow,omitempty"`
|
NetSoftnetAllow []string `json:"netSoftnetAllow,omitempty"`
|
||||||
NetSoftnetBlock []string `json:"netSoftnetBlock,omitempty"`
|
NetSoftnetBlock []string `json:"netSoftnetBlock,omitempty"`
|
||||||
Suspendable bool `json:"suspendable,omitempty"`
|
Suspendable bool `json:"suspendable,omitempty"`
|
||||||
|
USBAccessories bool `json:"usbAccessories,omitempty"`
|
||||||
PowerState PowerState `json:"powerState,omitempty"`
|
PowerState PowerState `json:"powerState,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user