Support running host processes alongside VMs (#482)

* Regenerate .pb with protoc-gen-go v1.36.11

* Support running host processes alongside VMs

* Replace existing host processes when starting a set
This commit is contained in:
edi-oai
2026-09-03 00:24:09 +01:00
committed by GitHub
parent 3fe0a284c5
commit a3e08ffcbf
33 changed files with 1454 additions and 309 deletions
+13
View File
@@ -169,6 +169,19 @@ func (service *VMsService) PortForward(
})
}
func (service *VMsService) PortForwardHostProcess(
ctx context.Context,
name string,
hostProcessName string,
waitSeconds uint16,
) (net.Conn, error) {
return service.client.wsRequest(ctx, fmt.Sprintf("vms/%s/port-forward", url.PathEscape(name)),
map[string]string{
"hostProcess": hostProcessName,
"wait": strconv.FormatUint(uint64(waitSeconds), 10),
})
}
func (service *VMsService) Exec(
ctx context.Context,
name string,
+9
View File
@@ -22,3 +22,12 @@ func TestSemanticallyEqualEquatesEmptySlices(t *testing.T) {
}
require.True(t, v1.SemanticallyEqual(nilSlicesSpec, emptySlicesSpec))
}
func TestVMSpecHostProcessesEqualEquatesEmptySlices(t *testing.T) {
nilHostProcessesSpec := v1.VMSpec{}
emptyHostProcessesSpec := v1.VMSpec{
HostProcesses: []v1.HostProcess{},
}
require.True(t, nilHostProcessesSpec.HostProcessesEqual(emptyHostProcessesSpec))
}
+36
View File
@@ -0,0 +1,36 @@
//nolint:err113 // Preserve host-process validation messages.
package v1
import (
"fmt"
"github.com/cirruslabs/orchard/internal/simplename"
mapset "github.com/deckarep/golang-set/v2"
)
// HostProcess describes a process run by the worker alongside a VM. Program is
// an executable path or a name resolved using the worker's PATH.
type HostProcess struct {
Name string `json:"name"`
Program string `json:"program"`
Args []string `json:"args,omitempty"`
Env map[string]string `json:"env,omitempty"`
}
func ValidateHostProcesses(processes []HostProcess) error {
seenNames := mapset.NewSetWithSize[string](len(processes))
for _, process := range processes {
if process.Name == "" || simplename.Validate(process.Name) != nil {
return fmt.Errorf("host process %q is invalid", process.Name)
}
if !seenNames.Add(process.Name) {
return fmt.Errorf("host process %q is duplicated", process.Name)
}
if process.Program == "" {
return fmt.Errorf("host process %q has an empty program", process.Name)
}
}
return nil
}
+13 -5
View File
@@ -10,11 +10,13 @@ var ErrUnsupportedServiceAccountRole = errors.New("unsupported service account r
type ServiceAccountRole string
const (
ServiceAccountRoleComputeRead ServiceAccountRole = "compute:read"
ServiceAccountRoleComputeWrite ServiceAccountRole = "compute:write"
ServiceAccountRoleComputeConnect ServiceAccountRole = "compute:connect"
ServiceAccountRoleAdminRead ServiceAccountRole = "admin:read"
ServiceAccountRoleAdminWrite ServiceAccountRole = "admin:write"
ServiceAccountRoleComputeRead ServiceAccountRole = "compute:read"
ServiceAccountRoleComputeWrite ServiceAccountRole = "compute:write"
ServiceAccountRoleComputeConnect ServiceAccountRole = "compute:connect"
ServiceAccountRoleHostProcessWrite ServiceAccountRole = "host-process:write"
ServiceAccountRoleHostProcessConnect ServiceAccountRole = "host-process:connect"
ServiceAccountRoleAdminRead ServiceAccountRole = "admin:read"
ServiceAccountRoleAdminWrite ServiceAccountRole = "admin:write"
)
func NewServiceAccountRole(name string) (ServiceAccountRole, error) {
@@ -25,6 +27,10 @@ func NewServiceAccountRole(name string) (ServiceAccountRole, error) {
return ServiceAccountRoleComputeWrite, nil
case string(ServiceAccountRoleComputeConnect):
return ServiceAccountRoleComputeConnect, nil
case string(ServiceAccountRoleHostProcessWrite):
return ServiceAccountRoleHostProcessWrite, nil
case string(ServiceAccountRoleHostProcessConnect):
return ServiceAccountRoleHostProcessConnect, nil
case string(ServiceAccountRoleAdminRead):
return ServiceAccountRoleAdminRead, nil
case string(ServiceAccountRoleAdminWrite):
@@ -39,6 +45,8 @@ func AllServiceAccountRoles() []ServiceAccountRole {
ServiceAccountRoleComputeRead,
ServiceAccountRoleComputeWrite,
ServiceAccountRoleComputeConnect,
ServiceAccountRoleHostProcessWrite,
ServiceAccountRoleHostProcessConnect,
ServiceAccountRoleAdminRead,
ServiceAccountRoleAdminWrite,
}
+9
View File
@@ -150,6 +150,9 @@ func (vm *VM) Validate() error {
switch vm.Runtime {
case RuntimeVetu:
if len(vm.HostProcesses) != 0 {
return unsupportedFieldError("hostProcesses")
}
if vm.NetSoftnetDeprecated || vm.NetSoftnet {
return unsupportedFieldError("netSoftnet")
}
@@ -189,6 +192,7 @@ type VMSpec struct {
// so this field defaults to that when not set.
Runtime Runtime `json:"runtime,omitempty"`
HostProcesses []HostProcess `json:"hostProcesses,omitempty"`
Endpoints []EndpointSpec `json:"endpoints,omitempty"`
NetSoftnetDeprecated bool `json:"net-softnet,omitempty"` //nolint:tagliatelle // legacy JSON key
NetSoftnet bool `json:"netSoftnet,omitempty"`
@@ -203,6 +207,11 @@ func SemanticallyEqual[T any](current, desired T) bool {
return cmp.Equal(current, desired, cmpopts.EquateEmpty())
}
func (vm VMSpec) HostProcessesEqual(other VMSpec) bool {
// Treat omitted and explicitly empty host process collections as the same specification
return cmp.Equal(vm.HostProcesses, other.HostProcesses, cmpopts.EquateEmpty())
}
func (vm VMSpec) SoftnetEnabled() bool {
return vm.NetSoftnetDeprecated || vm.NetSoftnet ||
len(vm.NetSoftnetAllow) != 0 || len(vm.NetSoftnetBlock) != 0
+3 -2
View File
@@ -13,8 +13,9 @@ type Condition struct {
type ConditionType string
const (
ConditionTypeScheduled ConditionType = "scheduled"
ConditionTypeRunning ConditionType = "running"
ConditionTypeScheduled ConditionType = "scheduled"
ConditionTypeRunning ConditionType = "running"
ConditionTypeHostProcessesReady ConditionType = "host-processes-ready"
ConditionTypeCloning ConditionType = "cloning"
ConditionTypeSuspending ConditionType = "suspending"
+14 -3
View File
@@ -1,3 +1,4 @@
//nolint:tagliatelle // Preserve the original vmUID keys for wire compatibility.
package v1
type WatchInstruction struct {
@@ -7,9 +8,19 @@ type WatchInstruction struct {
}
type PortForwardAction struct {
Session string `json:"session"`
VMUID string `json:"vmUID"`
Port uint16 `json:"port"`
Session string `json:"session"`
VMUID string `json:"vmUID"`
Port uint16 `json:"port"`
Target *PortForwardTarget `json:"target,omitempty"`
}
type PortForwardTarget struct {
HostProcess *PortForwardTargetHostProcess `json:"hostProcess,omitempty"`
}
type PortForwardTargetHostProcess struct {
VMUID string `json:"vmUID"`
Name string `json:"name"`
}
type SyncVMsAction struct {