Skip to content

Commit a4588aa

Browse files
committed
feat: add PodDisruptionBudget support for Karmada control plane components
- Add PodDisruptionBudgetConfig to CommonSettings API - Implement PDB creation/update/deletion logic for all components - Add validation for PDB configuration (mutual exclusivity, replicas check) - Support both minAvailable and maxUnavailable PDB strategies Signed-off-by: baiyutang <[email protected]>
1 parent d410a0b commit a4588aa

File tree

18 files changed

+832
-134
lines changed

18 files changed

+832
-134
lines changed

operator/pkg/apis/operator/v1alpha1/type.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package v1alpha1
1919
import (
2020
corev1 "k8s.io/api/core/v1"
2121
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
"k8s.io/apimachinery/pkg/util/intstr"
2223
)
2324

2425
// +genclient
@@ -700,6 +701,11 @@ type CommonSettings struct {
700701
// +kubebuilder:default="system-node-critical"
701702
// +optional
702703
PriorityClassName string `json:"priorityClassName,omitempty"`
704+
705+
// PodDisruptionBudgetConfig specifies the PodDisruptionBudget configuration
706+
// for this component's pods. If not set, no PDB will be created.
707+
// +optional
708+
PodDisruptionBudgetConfig *PodDisruptionBudgetConfig `json:"podDisruptionBudgetConfig,omitempty"`
703709
}
704710

705711
// Image allows to customize the image used for components.
@@ -793,6 +799,22 @@ type LocalSecretReference struct {
793799
Name string `json:"name,omitempty"`
794800
}
795801

802+
// PodDisruptionBudgetConfig defines a subset of PodDisruptionBudgetSpec fields
803+
// that users can configure for their control plane components.
804+
type PodDisruptionBudgetConfig struct {
805+
// MinAvailable specifies the minimum number or percentage of pods
806+
// that must remain available after evictions.
807+
// Mutually exclusive with MaxUnavailable.
808+
// +optional
809+
MinAvailable *intstr.IntOrString `json:"minAvailable,omitempty"`
810+
811+
// MaxUnavailable specifies the maximum number or percentage of pods
812+
// that can be unavailable after evictions.
813+
// Mutually exclusive with MinAvailable.
814+
// +optional
815+
MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"`
816+
}
817+
796818
// +kubebuilder:object:root=true
797819
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
798820

operator/pkg/constants/constants.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ const (
116116
// KarmadaMetricsAdapterComponent defines the name of the karmada-metrics-adapter component
117117
KarmadaMetricsAdapterComponent = "KarmadaMetricsAdapter"
118118

119+
// Component Label Values - Used for actual Kubernetes labels
120+
// These values match the app.kubernetes.io/name labels used in deployment templates
121+
KarmadaControllerManager = "karmada-controller-manager"
122+
KarmadaScheduler = "karmada-scheduler"
123+
KarmadaWebhook = "karmada-webhook"
124+
KarmadaSearch = "karmada-search"
125+
KarmadaDescheduler = "karmada-descheduler"
126+
KarmadaMetricsAdapter = "karmada-metrics-adapter"
127+
KarmadaAggregatedAPIServer = "karmada-aggregated-apiserver"
128+
119129
// KarmadaOperatorLabelKeyName defines a label key used by all resources created by karmada operator
120130
KarmadaOperatorLabelKeyName = "app.kubernetes.io/managed-by"
121131

operator/pkg/controller/karmada/validating.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1"
3131
"github.com/karmada-io/karmada/operator/pkg/util"
3232
"github.com/karmada-io/karmada/pkg/util/lifted"
33+
"k8s.io/apimachinery/pkg/util/intstr"
3334
)
3435

3536
func validateCRDTarball(crdTarball *operatorv1alpha1.CRDTarball, fldPath *field.Path) (errs field.ErrorList) {
@@ -97,6 +98,42 @@ func validateETCD(etcd *operatorv1alpha1.Etcd, karmadaName string, fldPath *fiel
9798
return errs
9899
}
99100

101+
// validateCommonSettings validates the common settings of a component, including PDB configuration
102+
func validateCommonSettings(commonSettings *operatorv1alpha1.CommonSettings, fldPath *field.Path) (errs field.ErrorList) {
103+
if commonSettings == nil {
104+
return nil
105+
}
106+
107+
if commonSettings.PodDisruptionBudgetConfig != nil {
108+
pdbConfig := commonSettings.PodDisruptionBudgetConfig
109+
pdbPath := fldPath.Child("podDisruptionBudgetConfig")
110+
111+
// Check if both minAvailable and maxUnavailable are set (mutually exclusive)
112+
if pdbConfig.MinAvailable != nil && pdbConfig.MaxUnavailable != nil {
113+
errs = append(errs, field.Invalid(pdbPath, pdbConfig, "minAvailable and maxUnavailable are mutually exclusive, only one can be set"))
114+
}
115+
116+
// Check if at least one of minAvailable or maxUnavailable is set
117+
if pdbConfig.MinAvailable == nil && pdbConfig.MaxUnavailable == nil {
118+
errs = append(errs, field.Invalid(pdbPath, pdbConfig, "either minAvailable or maxUnavailable must be set"))
119+
}
120+
121+
// Validate minAvailable against replicas if replicas is set
122+
if pdbConfig.MinAvailable != nil && commonSettings.Replicas != nil {
123+
replicas := *commonSettings.Replicas
124+
if pdbConfig.MinAvailable.Type == intstr.Int {
125+
minAvailable := int32(pdbConfig.MinAvailable.IntValue())
126+
if minAvailable > replicas {
127+
errs = append(errs, field.Invalid(pdbPath.Child("minAvailable"), pdbConfig.MinAvailable,
128+
fmt.Sprintf("minAvailable (%d) cannot be greater than replicas (%d)", minAvailable, replicas)))
129+
}
130+
}
131+
}
132+
}
133+
134+
return errs
135+
}
136+
100137
func validate(karmada *operatorv1alpha1.Karmada) error {
101138
var errs field.ErrorList
102139

@@ -107,6 +144,32 @@ func validate(karmada *operatorv1alpha1.Karmada) error {
107144

108145
errs = append(errs, validateKarmadaAPIServer(components.KarmadaAPIServer, karmada.Spec.HostCluster, fldPath.Child("karmadaAPIServer"))...)
109146
errs = append(errs, validateETCD(components.Etcd, karmada.Name, fldPath.Child("etcd"))...)
147+
148+
// Validate PDB configurations for all components
149+
if components.KarmadaAPIServer != nil {
150+
errs = append(errs, validateCommonSettings(&components.KarmadaAPIServer.CommonSettings, fldPath.Child("karmadaAPIServer"))...)
151+
}
152+
if components.KarmadaControllerManager != nil {
153+
errs = append(errs, validateCommonSettings(&components.KarmadaControllerManager.CommonSettings, fldPath.Child("karmadaControllerManager"))...)
154+
}
155+
if components.KarmadaScheduler != nil {
156+
errs = append(errs, validateCommonSettings(&components.KarmadaScheduler.CommonSettings, fldPath.Child("karmadaScheduler"))...)
157+
}
158+
if components.KarmadaWebhook != nil {
159+
errs = append(errs, validateCommonSettings(&components.KarmadaWebhook.CommonSettings, fldPath.Child("karmadaWebhook"))...)
160+
}
161+
if components.KarmadaDescheduler != nil {
162+
errs = append(errs, validateCommonSettings(&components.KarmadaDescheduler.CommonSettings, fldPath.Child("karmadaDescheduler"))...)
163+
}
164+
if components.KarmadaSearch != nil {
165+
errs = append(errs, validateCommonSettings(&components.KarmadaSearch.CommonSettings, fldPath.Child("karmadaSearch"))...)
166+
}
167+
if components.KarmadaMetricsAdapter != nil {
168+
errs = append(errs, validateCommonSettings(&components.KarmadaMetricsAdapter.CommonSettings, fldPath.Child("karmadaMetricsAdapter"))...)
169+
}
170+
if components.Etcd != nil && components.Etcd.Local != nil {
171+
errs = append(errs, validateCommonSettings(&components.Etcd.Local.CommonSettings, fldPath.Child("etcd").Child("local"))...)
172+
}
110173
}
111174

112175
if len(errs) > 0 {

operator/pkg/controller/karmada/validating_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import (
2020
"testing"
2121

2222
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/apimachinery/pkg/util/intstr"
24+
"k8s.io/apimachinery/pkg/util/validation/field"
25+
"k8s.io/utils/pointer"
2326

2427
operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1"
2528
"github.com/karmada-io/karmada/operator/pkg/util"
@@ -208,3 +211,102 @@ func Test_validate(t *testing.T) {
208211
})
209212
}
210213
}
214+
215+
func TestValidateCommonSettings(t *testing.T) {
216+
tests := []struct {
217+
name string
218+
commonSettings *operatorv1alpha1.CommonSettings
219+
expectedErrs int
220+
}{
221+
{
222+
name: "nil common settings",
223+
commonSettings: nil,
224+
expectedErrs: 0,
225+
},
226+
{
227+
name: "nil PDB config",
228+
commonSettings: &operatorv1alpha1.CommonSettings{
229+
Replicas: pointer.Int32(3),
230+
},
231+
expectedErrs: 0,
232+
},
233+
{
234+
name: "valid minAvailable config",
235+
commonSettings: &operatorv1alpha1.CommonSettings{
236+
Replicas: pointer.Int32(3),
237+
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
238+
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 2},
239+
},
240+
},
241+
expectedErrs: 0,
242+
},
243+
{
244+
name: "valid maxUnavailable config",
245+
commonSettings: &operatorv1alpha1.CommonSettings{
246+
Replicas: pointer.Int32(3),
247+
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
248+
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1},
249+
},
250+
},
251+
expectedErrs: 0,
252+
},
253+
{
254+
name: "valid percentage minAvailable config",
255+
commonSettings: &operatorv1alpha1.CommonSettings{
256+
Replicas: pointer.Int32(3),
257+
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
258+
MinAvailable: &intstr.IntOrString{Type: intstr.String, StrVal: "50%"},
259+
},
260+
},
261+
expectedErrs: 0,
262+
},
263+
{
264+
name: "both minAvailable and maxUnavailable set",
265+
commonSettings: &operatorv1alpha1.CommonSettings{
266+
Replicas: pointer.Int32(3),
267+
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
268+
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 2},
269+
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1},
270+
},
271+
},
272+
expectedErrs: 1,
273+
},
274+
{
275+
name: "neither minAvailable nor maxUnavailable set",
276+
commonSettings: &operatorv1alpha1.CommonSettings{
277+
Replicas: pointer.Int32(3),
278+
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{},
279+
},
280+
expectedErrs: 1,
281+
},
282+
{
283+
name: "minAvailable greater than replicas",
284+
commonSettings: &operatorv1alpha1.CommonSettings{
285+
Replicas: pointer.Int32(3),
286+
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
287+
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 4},
288+
},
289+
},
290+
expectedErrs: 1,
291+
},
292+
{
293+
name: "minAvailable equal to replicas",
294+
commonSettings: &operatorv1alpha1.CommonSettings{
295+
Replicas: pointer.Int32(3),
296+
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
297+
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 3},
298+
},
299+
},
300+
expectedErrs: 0,
301+
},
302+
}
303+
304+
for _, tt := range tests {
305+
t.Run(tt.name, func(t *testing.T) {
306+
errs := validateCommonSettings(tt.commonSettings, field.NewPath("test"))
307+
if len(errs) != tt.expectedErrs {
308+
t.Errorf("expected %d errors, got %d: %v", tt.expectedErrs, len(errs), errs)
309+
}
310+
})
311+
}
312+
}

operator/pkg/controlplane/apiserver/apiserver.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import (
2727
clientsetscheme "k8s.io/client-go/kubernetes/scheme"
2828

2929
operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1"
30+
"github.com/karmada-io/karmada/operator/pkg/constants"
3031
"github.com/karmada-io/karmada/operator/pkg/controlplane/etcd"
32+
"github.com/karmada-io/karmada/operator/pkg/controlplane/pdb"
3133
"github.com/karmada-io/karmada/operator/pkg/util"
3234
"github.com/karmada-io/karmada/operator/pkg/util/apiclient"
3335
"github.com/karmada-io/karmada/operator/pkg/util/patcher"
@@ -87,6 +89,12 @@ func installKarmadaAPIServer(client clientset.Interface, cfg *operatorv1alpha1.K
8789
if err := apiclient.CreateOrUpdateDeployment(client, apiserverDeployment); err != nil {
8890
return fmt.Errorf("error when creating deployment for %s, err: %w", apiserverDeployment.Name, err)
8991
}
92+
93+
// Ensure PDB for the apiserver component if configured
94+
if err := pdb.EnsurePodDisruptionBudget(constants.KarmadaAPIServer, name, namespace, &cfg.CommonSettings, client); err != nil {
95+
return fmt.Errorf("failed to ensure PDB for apiserver component, err: %w", err)
96+
}
97+
9098
return nil
9199
}
92100

@@ -158,6 +166,12 @@ func installKarmadaAggregatedAPIServer(client clientset.Interface, cfg *operator
158166
if err := apiclient.CreateOrUpdateDeployment(client, aggregatedAPIServerDeployment); err != nil {
159167
return fmt.Errorf("error when creating deployment for %s, err: %w", aggregatedAPIServerDeployment.Name, err)
160168
}
169+
170+
// Ensure PDB for the aggregated apiserver component if configured
171+
if err := pdb.EnsurePodDisruptionBudget(constants.KarmadaAggregatedAPIServer, name, namespace, &cfg.CommonSettings, client); err != nil {
172+
return fmt.Errorf("failed to ensure PDB for aggregated apiserver component, err: %w", err)
173+
}
174+
161175
return nil
162176
}
163177

0 commit comments

Comments
 (0)