Support scheduling by labels (#244)
This commit is contained in:
parent
581de320b9
commit
26c8808506
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -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"`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"`
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue