mirror of
https://github.com/cirruslabs/orchard.git
synced 2026-09-29 19:41:15 +02:00
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:
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user