Support scheduling by labels (#244)

This commit is contained in:
Nikolay Edigaryev 2025-02-06 18:05:36 +04:00 committed by GitHub
parent 581de320b9
commit 26c8808506
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 162 additions and 1 deletions

View File

@ -22,6 +22,7 @@ var headless bool
var username string var username string
var password string var password string
var resources map[string]string var resources map[string]string
var labels map[string]string
var restartPolicy string var restartPolicy string
var startupScript string var startupScript string
var hostDirsRaw []string var hostDirsRaw []string
@ -47,6 +48,8 @@ func newCreateVMCommand() *cobra.Command {
"SSH password to use when executing a startup script on the VM") "SSH password to use when executing a startup script on the VM")
command.PersistentFlags().StringToStringVar(&resources, "resources", map[string]string{}, command.PersistentFlags().StringToStringVar(&resources, "resources", map[string]string{},
"resources to request for this VM") "resources to request for this VM")
command.PersistentFlags().StringToStringVar(&labels, "labels", map[string]string{},
"labels required by this VM")
command.PersistentFlags().StringVar(&restartPolicy, "restart-policy", string(v1.RestartPolicyNever), command.PersistentFlags().StringVar(&restartPolicy, "restart-policy", string(v1.RestartPolicyNever),
fmt.Sprintf("restart policy for this VM: specify %q to never restart or %q "+ fmt.Sprintf("restart policy for this VM: specify %q to never restart or %q "+
"to only restart when the VM fails", v1.RestartPolicyNever, v1.RestartPolicyOnFailure)) "to only restart when the VM fails", v1.RestartPolicyNever, v1.RestartPolicyOnFailure))
@ -96,6 +99,7 @@ func runCreateVM(cmd *cobra.Command, args []string) error {
Headless: headless, Headless: headless,
Username: username, Username: username,
Password: password, Password: password,
Labels: labels,
HostDirs: hostDirs, HostDirs: hostDirs,
} }

View File

@ -28,6 +28,7 @@ var bootstrapTokenRaw string
var bootstrapTokenStdin bool var bootstrapTokenStdin bool
var logFilePath string var logFilePath string
var stringToStringResources map[string]string var stringToStringResources map[string]string
var labels map[string]string
var noPKI bool var noPKI bool
var defaultCPU uint64 var defaultCPU uint64
var defaultMemory uint64 var defaultMemory uint64
@ -51,6 +52,8 @@ func newRunCommand() *cobra.Command {
"optional path to a file where logs (up to 100 Mb) will be written.") "optional path to a file where logs (up to 100 Mb) will be written.")
cmd.PersistentFlags().StringToStringVar(&stringToStringResources, "resources", map[string]string{}, cmd.PersistentFlags().StringToStringVar(&stringToStringResources, "resources", map[string]string{},
"resources that this worker provides") "resources that this worker provides")
cmd.PersistentFlags().StringToStringVar(&labels, "labels", map[string]string{},
"labels that this worker supports")
cmd.PersistentFlags().BoolVar(&noPKI, "no-pki", false, cmd.PersistentFlags().BoolVar(&noPKI, "no-pki", false,
"do not use the host's root CA set and instead validate the Controller's presented "+ "do not use the host's root CA set and instead validate the Controller's presented "+
"certificate using a bootstrap token (or manually via fingerprint, "+ "certificate using a bootstrap token (or manually via fingerprint, "+
@ -122,6 +125,7 @@ func runWorker(cmd *cobra.Command, args []string) (err error) {
controllerClient, controllerClient,
worker.WithName(name), worker.WithName(name),
worker.WithResources(resources), worker.WithResources(resources),
worker.WithLabels(labels),
worker.WithDefaultCPUAndMemory(defaultCPU, defaultMemory), worker.WithDefaultCPUAndMemory(defaultCPU, defaultMemory),
worker.WithLogger(logger), worker.WithLogger(logger),
) )

View File

@ -98,6 +98,7 @@ func (controller *Controller) createWorker(ctx *gin.Context) responder.Responder
dbWorker.LastSeen = worker.LastSeen dbWorker.LastSeen = worker.LastSeen
dbWorker.Resources = worker.Resources dbWorker.Resources = worker.Resources
dbWorker.Labels = worker.Labels
dbWorker.DefaultCPU = worker.DefaultCPU dbWorker.DefaultCPU = worker.DefaultCPU
dbWorker.DefaultMemory = worker.DefaultMemory dbWorker.DefaultMemory = worker.DefaultMemory

View File

@ -235,7 +235,8 @@ NextVM:
if worker.Offline(scheduler.workerOfflineTimeout) || if worker.Offline(scheduler.workerOfflineTimeout) ||
worker.SchedulingPaused || worker.SchedulingPaused ||
!resourcesRemaining.CanFit(unscheduledVM.Resources) { !resourcesRemaining.CanFit(unscheduledVM.Resources) ||
!worker.Labels.Contains(unscheduledVM.Labels) {
continue NextWorker continue NextWorker
} }

View File

@ -0,0 +1,79 @@
package tests_test
import (
"context"
"github.com/cirruslabs/orchard/internal/tests/devcontroller"
"github.com/cirruslabs/orchard/internal/tests/wait"
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/stretchr/testify/require"
"testing"
"time"
)
func TestLabels(t *testing.T) {
ctx := context.Background()
// Create a development environment
devClient, _, _ := devcontroller.StartIntegrationTestEnvironmentWithAdditionalOpts(t,
false, nil,
true, nil,
)
// Create a worker that doesn't have any labels
_, err := devClient.Workers().Create(ctx, v1.Worker{
Meta: v1.Meta{
Name: "worker-without-labels",
},
Resources: map[string]uint64{
v1.ResourceTartVMs: 2,
},
})
require.NoError(t, err)
// Create a VM that requests a "role=test" label
vmName := "test-vm"
require.NoError(t, devClient.VMs().Create(ctx, &v1.VM{
Meta: v1.Meta{
Name: vmName,
},
Image: "example.com/doesnt/matter:latest",
Labels: map[string]string{"role": "test"},
Status: v1.VMStatusPending,
}))
// Ensure that this VM doesn't get assigned in 30 seconds
require.False(t, wait.Wait(30*time.Second, func() bool {
vm, err := devClient.VMs().Get(context.Background(), vmName)
require.NoError(t, err)
t.Logf("Waiting for the VM %s to be assigned", vmName)
return vm.Worker != ""
}), "VM %s was not expected to be assigned to any worker, but was assigned to some worker", vmName)
// Now create one more worker that has the required labels
const workerWithLabelsName = "worker-with-labels"
_, err = devClient.Workers().Create(ctx, v1.Worker{
Meta: v1.Meta{
Name: workerWithLabelsName,
},
Resources: map[string]uint64{
v1.ResourceTartVMs: 2,
},
Labels: map[string]string{"role": "test"},
})
require.NoError(t, err)
// Wait for the VM to be assigned to the new worker
require.True(t, wait.Wait(30*time.Second, func() bool {
vm, err := devClient.VMs().Get(context.Background(), vmName)
require.NoError(t, err)
t.Logf("Waiting for the VM %s to be assigned to a worker", vmName)
return vm.Worker == workerWithLabelsName
}), "VM was %s expected to be assigned to the worker %q, but was assigned "+
"to another worker (or no worker)", vmName, workerWithLabelsName)
}

View File

@ -19,6 +19,12 @@ func WithResources(resources v1.Resources) Option {
} }
} }
func WithLabels(labels v1.Labels) Option {
return func(worker *Worker) {
worker.labels = labels
}
}
func WithDefaultCPUAndMemory(defaultCPU uint64, defaultMemory uint64) Option { func WithDefaultCPUAndMemory(defaultCPU uint64, defaultMemory uint64) Option {
return func(worker *Worker) { return func(worker *Worker) {
worker.defaultCPU = defaultCPU worker.defaultCPU = defaultCPU

View File

@ -32,6 +32,7 @@ type Worker struct {
client *client.Client client *client.Client
pollTicker *time.Ticker pollTicker *time.Ticker
resources v1.Resources resources v1.Resources
labels v1.Labels
defaultCPU uint64 defaultCPU uint64
defaultMemory uint64 defaultMemory uint64
@ -195,6 +196,7 @@ func (worker *Worker) registerWorker(ctx context.Context) error {
Name: worker.name, Name: worker.name,
}, },
Resources: worker.resources, Resources: worker.resources,
Labels: worker.labels,
LastSeen: time.Now(), LastSeen: time.Now(),
MachineID: platformUUID, MachineID: platformUUID,
DefaultCPU: worker.defaultCPU, DefaultCPU: worker.defaultCPU,

13
pkg/resource/v1/labels.go Normal file
View File

@ -0,0 +1,13 @@
package v1
type Labels map[string]string
func (labels Labels) Contains(other Labels) bool {
for label, value := range other {
if labels[label] != value {
return false
}
}
return true
}

View File

@ -0,0 +1,45 @@
package v1_test
import (
v1 "github.com/cirruslabs/orchard/pkg/resource/v1"
"github.com/stretchr/testify/require"
"testing"
)
func TestLabelsMatch(t *testing.T) {
// Two nil labels
a := v1.Labels(nil)
b := v1.Labels(nil)
require.True(t, a.Contains(b))
require.True(t, b.Contains(a))
// Two empty labels
a = map[string]string{}
b = map[string]string{}
require.True(t, a.Contains(b))
require.True(t, b.Contains(a))
// Two identical labels
a = map[string]string{"foo": "bar"}
b = map[string]string{"foo": "bar"}
require.True(t, a.Contains(b))
require.True(t, b.Contains(a))
// Supersets against nil labels
a = v1.Labels(nil)
b = map[string]string{"baz": "qux", "foo": "bar"}
require.False(t, a.Contains(b))
require.True(t, b.Contains(a))
// Superset against empty labels
a = map[string]string{}
b = map[string]string{"baz": "qux", "foo": "bar"}
require.False(t, a.Contains(b))
require.True(t, b.Contains(a))
// Superset against subset labels
a = map[string]string{"foo": "bar"}
b = map[string]string{"baz": "qux", "foo": "bar"}
require.False(t, a.Contains(b))
require.True(t, b.Contains(a))
}

View File

@ -63,6 +63,9 @@ type VM struct {
// Resources required by this VM. // Resources required by this VM.
Resources Resources `json:"resources,omitempty"` Resources Resources `json:"resources,omitempty"`
// Labels required by this VM.
Labels Labels `json:"labels,omitempty"`
// HostDir is a list of host directories to be mounted to the VM. // HostDir is a list of host directories to be mounted to the VM.
HostDirs []HostDir `json:"hostDirs,omitempty"` HostDirs []HostDir `json:"hostDirs,omitempty"`

View File

@ -14,6 +14,9 @@ type Worker struct {
// Resources available on this Worker. // Resources available on this Worker.
Resources Resources `json:"resources,omitempty"` Resources Resources `json:"resources,omitempty"`
// Labels that this Worker supports.
Labels Labels `json:"labels,omitempty"`
// DefaultCPU is the amount of CPUs to assign to a VM // DefaultCPU is the amount of CPUs to assign to a VM
// when it doesn't explicitly request a specific amount. // when it doesn't explicitly request a specific amount.
DefaultCPU uint64 `json:"defaultCPU,omitempty"` DefaultCPU uint64 `json:"defaultCPU,omitempty"`