From 0983da3178ac93b880cd96d5f20588da6dd4d61d Mon Sep 17 00:00:00 2001 From: Matteo Bunino <48362942+matbun@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:15:33 +0200 Subject: [PATCH 1/2] feat: virtual nodes advertise ephemeral storage A virtual node advertised CPU, memory, pods and accelerators but no ephemeral-storage, so the scheduler never placed a pod that requests some: "Insufficient ephemeral-storage". Sidecars injected by other controllers often do; Katib's metrics collector requests 500Mi, which made every Katib trial routed to a virtual node stay Pending. Resources.EphemeralStorage sets the amount, 1Ti by default: the remote side has its own disks, the node only must not refuse such pods. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Matteo Bunino <48362942+matbun@users.noreply.github.com> --- pkg/virtualkubelet/config.go | 4 ++++ pkg/virtualkubelet/config_test.go | 12 ++++++++++++ pkg/virtualkubelet/virtualkubelet.go | 15 ++++++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/pkg/virtualkubelet/config.go b/pkg/virtualkubelet/config.go index e0426d33..899969cd 100644 --- a/pkg/virtualkubelet/config.go +++ b/pkg/virtualkubelet/config.go @@ -102,6 +102,10 @@ type Resources struct { Memory string `yaml:"Memory,omitempty"` // Pods specifies the maximum number of pods this node can handle Pods string `yaml:"Pods,omitempty"` + // EphemeralStorage is the ephemeral storage the node advertises (default 1Ti). A node + // that advertises none cannot take a pod that requests some, as sidecars often do + // (Katib's metrics collector requests 500Mi). + EphemeralStorage string `yaml:"EphemeralStorage,omitempty"` // Accelerators lists hardware accelerators available on this node Accelerators []Accelerator `yaml:"Accelerators"` } diff --git a/pkg/virtualkubelet/config_test.go b/pkg/virtualkubelet/config_test.go index 2550ab88..ab3a111b 100644 --- a/pkg/virtualkubelet/config_test.go +++ b/pkg/virtualkubelet/config_test.go @@ -464,3 +464,15 @@ func TestUpdateNodeTaints_NilLeavesExistingTaintsUnchangedAndUnknownEffectDefaul }, }, provider.node.Spec.Taints) } + +// A virtual node must advertise ephemeral storage, or a pod whose sidecar requests some +// (Katib's metrics collector: 500Mi) is never scheduled on it. +func TestGetResources_EphemeralStorage(t *testing.T) { + config := Config{Resources: Resources{CPU: "10", Memory: "10Gi", Pods: "10"}} + got := GetResources(config)[v1.ResourceEphemeralStorage] + assert.Equal(t, defaultEphemeralStorage, got.String()) + + config.Resources.EphemeralStorage = "50Gi" + got = GetResources(config)[v1.ResourceEphemeralStorage] + assert.Equal(t, "50Gi", got.String()) +} diff --git a/pkg/virtualkubelet/virtualkubelet.go b/pkg/virtualkubelet/virtualkubelet.go index 5b5f0076..c173d4c4 100644 --- a/pkg/virtualkubelet/virtualkubelet.go +++ b/pkg/virtualkubelet/virtualkubelet.go @@ -382,6 +382,10 @@ func NodeConditionWithInterlink(ready bool, interlinkStatus v1.ConditionStatus, return append(conditions, interlinkCondition) } +// defaultEphemeralStorage is advertised when Resources.EphemeralStorage is not set: the +// remote side has its own disks, so the node only must not refuse pods that ask for some. +const defaultEphemeralStorage = "1Ti" + func GetResources(config Config) v1.ResourceList { gpuCount := map[string]resource.Quantity{} fpgaCount := map[string]resource.Quantity{} @@ -401,10 +405,15 @@ func GetResources(config Config) v1.ResourceList { } } + ephemeralStorage := config.Resources.EphemeralStorage + if ephemeralStorage == "" { + ephemeralStorage = defaultEphemeralStorage + } resourceList := v1.ResourceList{ - "cpu": resource.MustParse(config.Resources.CPU), - "memory": resource.MustParse(config.Resources.Memory), - "pods": resource.MustParse(config.Resources.Pods), + "cpu": resource.MustParse(config.Resources.CPU), + "memory": resource.MustParse(config.Resources.Memory), + "pods": resource.MustParse(config.Resources.Pods), + v1.ResourceEphemeralStorage: resource.MustParse(ephemeralStorage), } for resourceType, q := range gpuCount { From bee981d62c3446b29851a782ec388cc0c21b5d40 Mon Sep 17 00:00:00 2001 From: Matteo Bunino <48362942+matbun@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:01:03 +0200 Subject: [PATCH 2/2] fix: reject an invalid Resources.EphemeralStorage at load time LoadConfig validated CPU, memory, pods and accelerators but not the new ephemeral storage value, so a value such as "50GB" passed and then made resource.MustParse panic while the provider started. It is now parsed in LoadConfig like the others; an empty value stays valid and keeps the default. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Matteo Bunino <48362942+matbun@users.noreply.github.com> --- pkg/virtualkubelet/config_test.go | 22 ++++++++++++++++++++++ pkg/virtualkubelet/virtualkubelet.go | 5 +++++ 2 files changed, 27 insertions(+) diff --git a/pkg/virtualkubelet/config_test.go b/pkg/virtualkubelet/config_test.go index ab3a111b..aeefc12c 100644 --- a/pkg/virtualkubelet/config_test.go +++ b/pkg/virtualkubelet/config_test.go @@ -2,10 +2,13 @@ package virtualkubelet import ( "context" + "os" + "path/filepath" "testing" types "github.com/interlink-hq/interlink/pkg/interlink" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" ) @@ -476,3 +479,22 @@ func TestGetResources_EphemeralStorage(t *testing.T) { got = GetResources(config)[v1.ResourceEphemeralStorage] assert.Equal(t, "50Gi", got.String()) } + +func TestLoadConfig_EphemeralStorage(t *testing.T) { + load := func(resources string) (Config, error) { + path := filepath.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(path, []byte("Resources:\n"+resources), 0o600)) + return LoadConfig(t.Context(), path) + } + + _, err := load(" EphemeralStorage: 50GB\n") + assert.ErrorContains(t, err, "invalid ephemeral storage value 50GB") + + config, err := load(" EphemeralStorage: 50Gi\n") + require.NoError(t, err) + assert.Equal(t, "50Gi", config.Resources.EphemeralStorage) + + config, err = load(" CPU: \"10\"\n") + require.NoError(t, err) + assert.Empty(t, config.Resources.EphemeralStorage) +} diff --git a/pkg/virtualkubelet/virtualkubelet.go b/pkg/virtualkubelet/virtualkubelet.go index c173d4c4..b637ac4c 100644 --- a/pkg/virtualkubelet/virtualkubelet.go +++ b/pkg/virtualkubelet/virtualkubelet.go @@ -659,6 +659,11 @@ func LoadConfig(ctx context.Context, providerConfig string) (config Config, err if _, err = resource.ParseQuantity(config.Resources.Pods); err != nil { return config, fmt.Errorf("invalid pods value %v", config.Resources.Pods) } + if config.Resources.EphemeralStorage != "" { + if _, err = resource.ParseQuantity(config.Resources.EphemeralStorage); err != nil { + return config, fmt.Errorf("invalid ephemeral storage value %v", config.Resources.EphemeralStorage) + } + } for _, accelerator := range config.Resources.Accelerators { if _, err = resource.ParseQuantity(accelerator.Available); err != nil { return config, fmt.Errorf("invalid value for accelerator %v (model: %v): %v", accelerator.ResourceType, accelerator.Model, err)