Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 22 additions & 0 deletions operator/pkg/apis/operator/v1alpha1/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)

// +genclient
Expand Down Expand Up @@ -700,6 +701,11 @@ type CommonSettings struct {
// +kubebuilder:default="system-node-critical"
// +optional
PriorityClassName string `json:"priorityClassName,omitempty"`

// PodDisruptionBudgetConfig specifies the PodDisruptionBudget configuration
// for this component's pods. If not set, no PDB will be created.
// +optional
PodDisruptionBudgetConfig *PodDisruptionBudgetConfig `json:"podDisruptionBudgetConfig,omitempty"`
}

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

// PodDisruptionBudgetConfig defines a subset of PodDisruptionBudgetSpec fields
// that users can configure for their control plane components.
type PodDisruptionBudgetConfig struct {
// MinAvailable specifies the minimum number or percentage of pods
// that must remain available after evictions.
// Mutually exclusive with MaxUnavailable.
// +optional
MinAvailable *intstr.IntOrString `json:"minAvailable,omitempty"`

// MaxUnavailable specifies the maximum number or percentage of pods
// that can be unavailable after evictions.
// Mutually exclusive with MinAvailable.
// +optional
MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"`
}

// +kubebuilder:object:root=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

Expand Down
10 changes: 10 additions & 0 deletions operator/pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ const (
// KarmadaMetricsAdapterComponent defines the name of the karmada-metrics-adapter component
KarmadaMetricsAdapterComponent = "KarmadaMetricsAdapter"

// Component Label Values - Used for actual Kubernetes labels
// These values match the app.kubernetes.io/name labels used in deployment templates
KarmadaControllerManager = "karmada-controller-manager"
KarmadaScheduler = "karmada-scheduler"
KarmadaWebhook = "karmada-webhook"
KarmadaSearch = "karmada-search"
KarmadaDescheduler = "karmada-descheduler"
KarmadaMetricsAdapter = "karmada-metrics-adapter"
KarmadaAggregatedAPIServer = "karmada-aggregated-apiserver"

// KarmadaOperatorLabelKeyName defines a label key used by all resources created by karmada operator
KarmadaOperatorLabelKeyName = "app.kubernetes.io/managed-by"

Expand Down
63 changes: 63 additions & 0 deletions operator/pkg/controller/karmada/validating.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1"
"github.com/karmada-io/karmada/operator/pkg/util"
"github.com/karmada-io/karmada/pkg/util/lifted"
"k8s.io/apimachinery/pkg/util/intstr"
Copy link
Preview

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The import k8s.io/apimachinery/pkg/util/intstr should be moved up to maintain alphabetical ordering of imports within the same group.

Copilot uses AI. Check for mistakes.

)

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

// validateCommonSettings validates the common settings of a component, including PDB configuration
func validateCommonSettings(commonSettings *operatorv1alpha1.CommonSettings, fldPath *field.Path) (errs field.ErrorList) {
if commonSettings == nil {
return nil
}

if commonSettings.PodDisruptionBudgetConfig != nil {
pdbConfig := commonSettings.PodDisruptionBudgetConfig
pdbPath := fldPath.Child("podDisruptionBudgetConfig")

// Check if both minAvailable and maxUnavailable are set (mutually exclusive)
if pdbConfig.MinAvailable != nil && pdbConfig.MaxUnavailable != nil {
errs = append(errs, field.Invalid(pdbPath, pdbConfig, "minAvailable and maxUnavailable are mutually exclusive, only one can be set"))
}

// Check if at least one of minAvailable or maxUnavailable is set
if pdbConfig.MinAvailable == nil && pdbConfig.MaxUnavailable == nil {
errs = append(errs, field.Invalid(pdbPath, pdbConfig, "either minAvailable or maxUnavailable must be set"))
}

// Validate minAvailable against replicas if replicas is set
if pdbConfig.MinAvailable != nil && commonSettings.Replicas != nil {
replicas := *commonSettings.Replicas
if pdbConfig.MinAvailable.Type == intstr.Int {
minAvailable := int32(pdbConfig.MinAvailable.IntValue())
if minAvailable > replicas {
errs = append(errs, field.Invalid(pdbPath.Child("minAvailable"), pdbConfig.MinAvailable,
fmt.Sprintf("minAvailable (%d) cannot be greater than replicas (%d)", minAvailable, replicas)))
}
}
}
}

return errs
}

func validate(karmada *operatorv1alpha1.Karmada) error {
var errs field.ErrorList

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

errs = append(errs, validateKarmadaAPIServer(components.KarmadaAPIServer, karmada.Spec.HostCluster, fldPath.Child("karmadaAPIServer"))...)
errs = append(errs, validateETCD(components.Etcd, karmada.Name, fldPath.Child("etcd"))...)

// Validate PDB configurations for all components
if components.KarmadaAPIServer != nil {
errs = append(errs, validateCommonSettings(&components.KarmadaAPIServer.CommonSettings, fldPath.Child("karmadaAPIServer"))...)
}
if components.KarmadaControllerManager != nil {
errs = append(errs, validateCommonSettings(&components.KarmadaControllerManager.CommonSettings, fldPath.Child("karmadaControllerManager"))...)
}
if components.KarmadaScheduler != nil {
errs = append(errs, validateCommonSettings(&components.KarmadaScheduler.CommonSettings, fldPath.Child("karmadaScheduler"))...)
}
if components.KarmadaWebhook != nil {
errs = append(errs, validateCommonSettings(&components.KarmadaWebhook.CommonSettings, fldPath.Child("karmadaWebhook"))...)
}
if components.KarmadaDescheduler != nil {
errs = append(errs, validateCommonSettings(&components.KarmadaDescheduler.CommonSettings, fldPath.Child("karmadaDescheduler"))...)
}
if components.KarmadaSearch != nil {
errs = append(errs, validateCommonSettings(&components.KarmadaSearch.CommonSettings, fldPath.Child("karmadaSearch"))...)
}
if components.KarmadaMetricsAdapter != nil {
errs = append(errs, validateCommonSettings(&components.KarmadaMetricsAdapter.CommonSettings, fldPath.Child("karmadaMetricsAdapter"))...)
}
if components.Etcd != nil && components.Etcd.Local != nil {
errs = append(errs, validateCommonSettings(&components.Etcd.Local.CommonSettings, fldPath.Child("etcd").Child("local"))...)
}
}

if len(errs) > 0 {
Expand Down
102 changes: 102 additions & 0 deletions operator/pkg/controller/karmada/validating_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import (
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/pointer"
Copy link
Preview

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import should be k8s.io/utils/ptr instead of k8s.io/utils/pointer which is deprecated. Also, the function call should be updated from pointer.Int32(3) to ptr.To[int32](3).

Suggested change
"k8s.io/utils/pointer"
"k8s.io/utils/ptr"

Copilot uses AI. Check for mistakes.


operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1"
"github.com/karmada-io/karmada/operator/pkg/util"
Expand Down Expand Up @@ -208,3 +211,102 @@ func Test_validate(t *testing.T) {
})
}
}

func TestValidateCommonSettings(t *testing.T) {
tests := []struct {
name string
commonSettings *operatorv1alpha1.CommonSettings
expectedErrs int
}{
{
name: "nil common settings",
commonSettings: nil,
expectedErrs: 0,
},
{
name: "nil PDB config",
commonSettings: &operatorv1alpha1.CommonSettings{
Replicas: pointer.Int32(3),
},
expectedErrs: 0,
},
{
name: "valid minAvailable config",
commonSettings: &operatorv1alpha1.CommonSettings{
Replicas: pointer.Int32(3),
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 2},
},
},
expectedErrs: 0,
},
{
name: "valid maxUnavailable config",
commonSettings: &operatorv1alpha1.CommonSettings{
Replicas: pointer.Int32(3),
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1},
},
},
expectedErrs: 0,
},
{
name: "valid percentage minAvailable config",
commonSettings: &operatorv1alpha1.CommonSettings{
Replicas: pointer.Int32(3),
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
MinAvailable: &intstr.IntOrString{Type: intstr.String, StrVal: "50%"},
},
},
expectedErrs: 0,
},
{
name: "both minAvailable and maxUnavailable set",
commonSettings: &operatorv1alpha1.CommonSettings{
Replicas: pointer.Int32(3),
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 2},
MaxUnavailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 1},
},
},
expectedErrs: 1,
},
{
name: "neither minAvailable nor maxUnavailable set",
commonSettings: &operatorv1alpha1.CommonSettings{
Replicas: pointer.Int32(3),
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{},
},
expectedErrs: 1,
},
{
name: "minAvailable greater than replicas",
commonSettings: &operatorv1alpha1.CommonSettings{
Replicas: pointer.Int32(3),
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 4},
},
},
expectedErrs: 1,
},
{
name: "minAvailable equal to replicas",
commonSettings: &operatorv1alpha1.CommonSettings{
Replicas: pointer.Int32(3),
PodDisruptionBudgetConfig: &operatorv1alpha1.PodDisruptionBudgetConfig{
MinAvailable: &intstr.IntOrString{Type: intstr.Int, IntVal: 3},
},
},
expectedErrs: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errs := validateCommonSettings(tt.commonSettings, field.NewPath("test"))
if len(errs) != tt.expectedErrs {
t.Errorf("expected %d errors, got %d: %v", tt.expectedErrs, len(errs), errs)
}
})
}
}
14 changes: 14 additions & 0 deletions operator/pkg/controlplane/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import (
clientsetscheme "k8s.io/client-go/kubernetes/scheme"

operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1"
"github.com/karmada-io/karmada/operator/pkg/constants"
"github.com/karmada-io/karmada/operator/pkg/controlplane/etcd"
"github.com/karmada-io/karmada/operator/pkg/controlplane/pdb"
"github.com/karmada-io/karmada/operator/pkg/util"
"github.com/karmada-io/karmada/operator/pkg/util/apiclient"
"github.com/karmada-io/karmada/operator/pkg/util/patcher"
Expand Down Expand Up @@ -87,6 +89,12 @@ func installKarmadaAPIServer(client clientset.Interface, cfg *operatorv1alpha1.K
if err := apiclient.CreateOrUpdateDeployment(client, apiserverDeployment); err != nil {
return fmt.Errorf("error when creating deployment for %s, err: %w", apiserverDeployment.Name, err)
}

// Ensure PDB for the apiserver component if configured
if err := pdb.EnsurePodDisruptionBudget(constants.KarmadaAPIServer, name, namespace, &cfg.CommonSettings, client); err != nil {
return fmt.Errorf("failed to ensure PDB for apiserver component, err: %w", err)
}

return nil
}

Expand Down Expand Up @@ -158,6 +166,12 @@ func installKarmadaAggregatedAPIServer(client clientset.Interface, cfg *operator
if err := apiclient.CreateOrUpdateDeployment(client, aggregatedAPIServerDeployment); err != nil {
return fmt.Errorf("error when creating deployment for %s, err: %w", aggregatedAPIServerDeployment.Name, err)
}

// Ensure PDB for the aggregated apiserver component if configured
if err := pdb.EnsurePodDisruptionBudget(constants.KarmadaAggregatedAPIServer, name, namespace, &cfg.CommonSettings, client); err != nil {
return fmt.Errorf("failed to ensure PDB for aggregated apiserver component, err: %w", err)
}

return nil
}

Expand Down
Loading
Loading