Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/Added-20260802-185014.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Added
body: Added support for configuring binder Pod Disruption Budget via Helm values (`binder.podDisruptionBudget`) when running multiple replicas
time: 2026-08-02T18:50:14.477675704+01:00
custom:
Author: dttung2905
Issue: "1477"
9 changes: 9 additions & 0 deletions deployments/kai-scheduler/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,15 @@ spec:
affinity:
{{- toYaml .Values.binder.affinity | nindent 8 }}
{{- end }}
{{- if .Values.binder.podDisruptionBudget }}
podDisruptionBudget:
{{- if hasKey .Values.binder.podDisruptionBudget "enabled" }}
enabled: {{ .Values.binder.podDisruptionBudget.enabled }}
{{- end }}
{{- if hasKey .Values.binder.podDisruptionBudget "maxUnavailable" }}
maxUnavailable: {{ .Values.binder.podDisruptionBudget.maxUnavailable }}
{{- end }}
{{- end }}
metricsPort: {{ .Values.binder.ports.metricsPort }}
resourceReservation:
{{- if .Values.global.resourceReservation.namespace }}
Expand Down
68 changes: 68 additions & 0 deletions deployments/kai-scheduler/tests/binder_pdb_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copyright 2025 NVIDIA CORPORATION
# SPDX-License-Identifier: Apache-2.0

suite: binder podDisruptionBudget in kai-config

set:
kaiConfigDeployer:
enabled: false
kaiConfig:
render: true

templates:
- kai-config.yaml
tests:
- it: renders binder podDisruptionBudget from values
set:
binder.podDisruptionBudget:
enabled: true
maxUnavailable: 1
asserts:
- equal:
path: spec.binder.service.podDisruptionBudget.enabled
value: true
- equal:
path: spec.binder.service.podDisruptionBudget.maxUnavailable
value: 1

- it: renders disabled PDB when enabled is false
set:
binder.podDisruptionBudget:
enabled: false
asserts:
- equal:
path: spec.binder.service.podDisruptionBudget.enabled
value: false

- it: omits podDisruptionBudget when the values map is absent
set:
binder.podDisruptionBudget: null
asserts:
- isNull:
path: spec.binder.service.podDisruptionBudget

- it: renders explicit zero maxUnavailable
set:
binder.podDisruptionBudget:
enabled: true
maxUnavailable: 0
asserts:
- equal:
path: spec.binder.service.podDisruptionBudget.enabled
value: true
- equal:
path: spec.binder.service.podDisruptionBudget.maxUnavailable
value: 0

- it: renders custom maxUnavailable
set:
binder.podDisruptionBudget:
enabled: true
maxUnavailable: 2
asserts:
- equal:
path: spec.binder.service.podDisruptionBudget.enabled
value: true
- equal:
path: spec.binder.service.podDisruptionBudget.maxUnavailable
value: 2
5 changes: 5 additions & 0 deletions deployments/kai-scheduler/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ binder:
ports:
metricsPort: 8080
affinity: {}
# PDB is created only when binder replicas > 1 (operator.replicaCount / binder.replicas).
# For HA: set operator.replicaCount: 2 (or override binder.replicas in Config).
podDisruptionBudget:
enabled: true
maxUnavailable: 1
# plugins allows overriding binder plugin configuration.
# Built-in plugins can be disabled, reordered, or have their arguments changed.
# Default plugins and priorities: volumebinding=300, dynamicresources=200, gpusharing=100.
Expand Down
1 change: 1 addition & 0 deletions pkg/operator/operands/binder/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (b *Binder) DesiredState(
objects := []client.Object{}
for _, resourceFunc := range []resourceForKAIConfig{
b.deploymentForKAIConfig,
b.podDisruptionBudgetForKAIConfig,
b.serviceAccountForKAIConfig,
b.serviceForKAIConfig,
resourceReservationServiceAccount,
Expand Down
130 changes: 130 additions & 0 deletions pkg/operator/operands/binder/binder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ import (

kaiv1 "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1"
kaiv1binder "github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1/binder"
"github.com/kai-scheduler/KAI-scheduler/pkg/apis/kai/v1/common"
binderplugins "github.com/kai-scheduler/KAI-scheduler/pkg/binder/plugins"
"github.com/kai-scheduler/KAI-scheduler/pkg/common/constants"
"github.com/kai-scheduler/KAI-scheduler/pkg/operator/operands/common/test_utils"

appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -328,6 +331,133 @@ var _ = Describe("Binder", func() {
Expect(newReservationSA.ImagePullSecrets).To(ContainElement(v1.LocalObjectReference{Name: "test-secret"}))
})
})

Context("PodDisruptionBudget", func() {
It("includes PDB when HA and enabled with matching deployment selector", func(ctx context.Context) {
kaiConfig.Spec.Binder.Replicas = ptr.To(int32(2))
kaiConfig.Spec.Binder.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
Enabled: ptr.To(true),
MaxUnavailable: ptr.To(int32(1)),
}

objects, err := b.DesiredState(ctx, fakeKubeClient, kaiConfig)
Expect(err).To(BeNil())

pdbs := test_utils.FindTypesInObjects[*policyv1.PodDisruptionBudget](objects)
Expect(pdbs).To(HaveLen(1))
Expect(pdbs[0].Name).To(Equal(defaultResourceName))
Expect(pdbs[0].Namespace).To(Equal(constants.DefaultKAINamespace))
Expect(pdbs[0].Spec.MaxUnavailable).NotTo(BeNil())
Expect(pdbs[0].Spec.MaxUnavailable.IntVal).To(Equal(int32(1)))
Expect(pdbs[0].Spec.Selector.MatchLabels["app"]).To(Equal(defaultResourceName))

deploymentT := test_utils.FindTypeInObjects[*appsv1.Deployment](objects)
Expect(deploymentT).NotTo(BeNil())
Expect(pdbs[0].Spec.Selector.MatchLabels["app"]).To(Equal((*deploymentT).Spec.Template.Labels["app"]))
})

It("uses custom maxUnavailable", func(ctx context.Context) {
b.BaseResourceName = defaultResourceName
kaiConfig.Spec.Binder.Replicas = ptr.To(int32(2))
kaiConfig.Spec.Binder.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
Enabled: ptr.To(true),
MaxUnavailable: ptr.To(int32(2)),
}

objects, err := b.podDisruptionBudgetForKAIConfig(ctx, fakeKubeClient, kaiConfig)
Expect(err).To(BeNil())
Expect(objects).To(HaveLen(1))

pdb := objects[0].(*policyv1.PodDisruptionBudget)
Expect(pdb.Spec.MaxUnavailable).NotTo(BeNil())
Expect(pdb.Spec.MaxUnavailable.IntVal).To(Equal(int32(2)))
})

It("preserves resourceVersion from an existing PDB", func(ctx context.Context) {
existing := &policyv1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Name: defaultResourceName,
Namespace: constants.DefaultKAINamespace,
ResourceVersion: "42",
Labels: map[string]string{
"app": defaultResourceName,
},
},
}
fakeKubeClient = fake.NewClientBuilder().WithObjects(existing).Build()
b.BaseResourceName = defaultResourceName

kaiConfig.Spec.Binder.Replicas = ptr.To(int32(2))
kaiConfig.Spec.Binder.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
Enabled: ptr.To(true),
MaxUnavailable: ptr.To(int32(1)),
}

objects, err := b.podDisruptionBudgetForKAIConfig(ctx, fakeKubeClient, kaiConfig)
Expect(err).To(BeNil())
Expect(objects).To(HaveLen(1))

pdb := objects[0].(*policyv1.PodDisruptionBudget)
Expect(pdb.ResourceVersion).To(Equal("42"))
Expect(pdb.Spec.MaxUnavailable).NotTo(BeNil())
Expect(pdb.Spec.MaxUnavailable.IntVal).To(Equal(int32(1)))
})

It("omits PDB when HA but disabled", func(ctx context.Context) {
kaiConfig.Spec.Binder.Replicas = ptr.To(int32(2))
kaiConfig.Spec.Binder.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
Enabled: ptr.To(false),
}

objects, err := b.DesiredState(ctx, fakeKubeClient, kaiConfig)
Expect(err).To(BeNil())

for _, obj := range objects {
Expect(obj).NotTo(BeAssignableToTypeOf(&policyv1.PodDisruptionBudget{}))
}
})

It("omits PDB when single replica even if enabled", func(ctx context.Context) {
kaiConfig.Spec.Binder.Replicas = ptr.To(int32(1))
kaiConfig.Spec.Binder.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
Enabled: ptr.To(true),
}

objects, err := b.DesiredState(ctx, fakeKubeClient, kaiConfig)
Expect(err).To(BeNil())

for _, obj := range objects {
Expect(obj).NotTo(BeAssignableToTypeOf(&policyv1.PodDisruptionBudget{}))
}
})

It("omits PDB when PDB config is missing and defaults apply", func(ctx context.Context) {
kaiConfig.Spec.Binder.Replicas = ptr.To(int32(2))
kaiConfig.Spec.Binder.Service.PodDisruptionBudget = nil
kaiConfig.Spec.Binder.Service.SetDefaultsWhereNeeded("")

objects, err := b.DesiredState(ctx, fakeKubeClient, kaiConfig)
Expect(err).To(BeNil())
Expect(kaiConfig.Spec.Binder.Service.PodDisruptionBudget).NotTo(BeNil())
Expect(*kaiConfig.Spec.Binder.Service.PodDisruptionBudget.Enabled).To(BeFalse())

for _, obj := range objects {
Expect(obj).NotTo(BeAssignableToTypeOf(&policyv1.PodDisruptionBudget{}))
}
})

It("returns empty desired state when binder is disabled", func(ctx context.Context) {
kaiConfig.Spec.Binder.Service.Enabled = ptr.To(false)
kaiConfig.Spec.Binder.Replicas = ptr.To(int32(2))
kaiConfig.Spec.Binder.Service.PodDisruptionBudget = &common.PodDisruptionBudget{
Enabled: ptr.To(true),
}

objects, err := b.DesiredState(ctx, fakeKubeClient, kaiConfig)
Expect(err).To(BeNil())
Expect(objects).To(BeEmpty())
})
})
})
})

Expand Down
22 changes: 22 additions & 0 deletions pkg/operator/operands/binder/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ func (b *Binder) serviceAccountForKAIConfig(
return []client.Object{sa}, err
}

func (b *Binder) podDisruptionBudgetForKAIConfig(
ctx context.Context, runtimeClient client.Reader, kaiConfig *kaiv1.Config,
) ([]client.Object, error) {
config := kaiConfig.Spec.Binder
pdbObj, err := common.PodDisruptionBudgetForKAIConfig(
ctx,
runtimeClient,
kaiConfig.Spec.Namespace,
b.BaseResourceName,
config.Replicas,
config.Service,
)
if err != nil {
return nil, err
}
if pdbObj == nil {
return nil, nil
}

return []client.Object{pdbObj}, nil
}

func (b *Binder) serviceForKAIConfig(
ctx context.Context, runtimeClient client.Reader, kaiConfig *kaiv1.Config,
) ([]client.Object, error) {
Expand Down
1 change: 1 addition & 0 deletions pkg/operator/operands/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ var PodDisruptionBudgetImplementedServices = map[string]struct{}{
"admission": {},
"scheduler": {},
"pod-grouper": {},
"binder": {},
}

func PodDisruptionBudgetImplemented(serviceName string) bool {
Expand Down
5 changes: 3 additions & 2 deletions pkg/operator/operands/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,10 +576,11 @@ var _ = Describe("PodDisruptionBudgetForKAIConfig", func() {

var _ = Describe("PodDisruptionBudgetImplementedServices", func() {
It("only lists operands with operator-side PDB creation", func() {
Expect(PodDisruptionBudgetImplementedServices).To(HaveLen(3))
Expect(PodDisruptionBudgetImplementedServices).To(HaveLen(4))
Expect(PodDisruptionBudgetImplemented("admission")).To(BeTrue())
Expect(PodDisruptionBudgetImplemented("scheduler")).To(BeTrue())
Expect(PodDisruptionBudgetImplemented("pod-grouper")).To(BeTrue())
Expect(PodDisruptionBudgetImplemented("binder")).To(BeFalse())
Expect(PodDisruptionBudgetImplemented("binder")).To(BeTrue())
Expect(PodDisruptionBudgetImplemented("queue-controller")).To(BeFalse())
})
})
Loading