Skip to content
Closed
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
240 changes: 240 additions & 0 deletions charts/karmada-operator/crds/operator.karmada.io_karmadas.yaml

Large diffs are not rendered by default.

240 changes: 240 additions & 0 deletions operator/config/crds/operator.karmada.io_karmadas.yaml

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions operator/config/samples/karmada-sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ spec:
imageRepository: registry.k8s.io/etcd
imageTag: 3.5.16-0
replicas: 1
podDisruptionBudgetConfig:
minAvailable: 1
volumeData:
# hostPath:
# type: DirectoryOrCreate
Expand All @@ -34,27 +36,41 @@ spec:
replicas: 1
serviceType: NodePort
serviceSubnet: 10.96.0.0/12
podDisruptionBudgetConfig:
minAvailable: 1
karmadaAggregatedAPIServer:
imageRepository: docker.io/karmada/karmada-aggregated-apiserver
imageTag: {{image_tag}}
replicas: 1
podDisruptionBudgetConfig:
minAvailable: 1
karmadaControllerManager:
imageRepository: docker.io/karmada/karmada-controller-manager
imageTag: {{image_tag}}
replicas: 1
podDisruptionBudgetConfig:
minAvailable: 1
karmadaScheduler:
imageRepository: docker.io/karmada/karmada-scheduler
imageTag: {{image_tag}}
replicas: 1
podDisruptionBudgetConfig:
minAvailable: 1
karmadaWebhook:
imageRepository: docker.io/karmada/karmada-webhook
imageTag: {{image_tag}}
replicas: 1
podDisruptionBudgetConfig:
minAvailable: 1
kubeControllerManager:
imageRepository: registry.k8s.io/kube-controller-manager
imageTag: v1.31.3
replicas: 1
podDisruptionBudgetConfig:
minAvailable: 1
karmadaMetricsAdapter:
imageRepository: docker.io/karmada/karmada-metrics-adapter
imageTag: {{image_tag}}
replicas: 2
podDisruptionBudgetConfig:
minAvailable: 1
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 @@ -685,6 +686,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 @@ -778,6 +784,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
32 changes: 32 additions & 0 deletions operator/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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

// KarmadaWebhook defines the name of the karmada-webhook component
KarmadaWebhook = "karmada-webhook"
// KarmadaSearch defines the name of the karmada-search component
KarmadaSearch = "karmada-search"
// KarmadaMetricsAdapter defines the name of the karmada-metrics-adapter component
KarmadaMetricsAdapter = "karmada-metrics-adapter"
// KarmadaAggregatedAPIServer defines the name of the karmada-aggregated-apiserver component
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
158 changes: 158 additions & 0 deletions operator/pkg/controller/karmada/validating.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/klog/v2"

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 {
minAvailableInt := pdbConfig.MinAvailable.IntValue()
if minAvailableInt > int(replicas) {
errs = append(errs, field.Invalid(pdbPath.Child("minAvailable"), pdbConfig.MinAvailable,
fmt.Sprintf("minAvailable (%d) cannot be greater than replicas (%d)", minAvailableInt, replicas)))
}
}
}
}

return errs
}

Comment on lines +101 to +136
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

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

The validateCommonSettings function is defined but never used in the codebase. This function appears to duplicate the validation logic in checkPdb (lines 157-204) and validatePDBConfigs (lines 206-274). Consider removing this unused function to reduce code duplication.

Suggested change
// 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 {
minAvailableInt := pdbConfig.MinAvailable.IntValue()
if minAvailableInt > int(replicas) {
errs = append(errs, field.Invalid(pdbPath.Child("minAvailable"), pdbConfig.MinAvailable,
fmt.Sprintf("minAvailable (%d) cannot be greater than replicas (%d)", minAvailableInt, replicas)))
}
}
}
}
return errs
}

Copilot uses AI. Check for mistakes.
func validate(karmada *operatorv1alpha1.Karmada) error {
var errs field.ErrorList

Expand All @@ -107,6 +144,8 @@ 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 configs
errs = append(errs, validatePDBConfigs(components, fldPath)...)
}

if len(errs) > 0 {
Expand All @@ -115,6 +154,125 @@ func validate(karmada *operatorv1alpha1.Karmada) error {
return nil
}

func checkPdb(path *field.Path, cfg *operatorv1alpha1.PodDisruptionBudgetConfig, replicas *int32) (errs field.ErrorList) {
if cfg == nil {
return
}
// must set either minAvailable or maxUnavailable
if cfg.MinAvailable == nil && cfg.MaxUnavailable == nil {
errs = append(errs, field.Invalid(path, cfg, "either minAvailable or maxUnavailable must be set"))
return
}
// cannot set both
if cfg.MinAvailable != nil && cfg.MaxUnavailable != nil {
errs = append(errs, field.Invalid(path, cfg, "minAvailable and maxUnavailable are mutually exclusive"))
}

// default replicas=1 if unset
replicaCount := int32(1)
if replicas != nil {
replicaCount = *replicas
}

// validate minAvailable
if cfg.MinAvailable != nil {
val, err := intstr.GetScaledValueFromIntOrPercent(cfg.MinAvailable, int(replicaCount), false)
if err != nil {
errs = append(errs, field.Invalid(path.Child("minAvailable"), cfg.MinAvailable, "invalid percentage or integer"))
} else if val > int(replicaCount) {
errs = append(errs, field.Invalid(
path.Child("minAvailable"), cfg.MinAvailable,
fmt.Sprintf("minAvailable %d cannot be greater than replicas %d", val, replicaCount),
))
}
}

// validate maxUnavailable
if cfg.MaxUnavailable != nil {
val, err := intstr.GetScaledValueFromIntOrPercent(cfg.MaxUnavailable, int(replicaCount), true)
if err != nil {
errs = append(errs, field.Invalid(path.Child("maxUnavailable"), cfg.MaxUnavailable, "invalid percentage or integer"))
} else if val > int(replicaCount) {
errs = append(errs, field.Invalid(
path.Child("maxUnavailable"), cfg.MaxUnavailable,
fmt.Sprintf("maxUnavailable %d cannot be greater than replicas %d", val, replicaCount),
))
}
}

return errs
}

// validatePDBConfigs ensures each component's PodDisruptionBudgetConfig is well-formed
// and consistent with the configured replica count.
func validatePDBConfigs(components *operatorv1alpha1.KarmadaComponents, fldPath *field.Path) (errs field.ErrorList) {
if components == nil {
return nil
}

if components.Etcd != nil && components.Etcd.Local != nil {
checkPdb(fldPath.Child("etcd").Child("podDisruptionBudgetConfig"),
components.Etcd.Local.PodDisruptionBudgetConfig,
components.Etcd.Local.Replicas,
)
}
if components.KarmadaAPIServer != nil {
checkPdb(fldPath.Child("karmadaAPIServer").Child("podDisruptionBudgetConfig"),
components.KarmadaAPIServer.PodDisruptionBudgetConfig,
components.KarmadaAPIServer.Replicas,
)
}
if components.KarmadaAggregatedAPIServer != nil {
checkPdb(fldPath.Child("karmadaAggregatedAPIServer").Child("podDisruptionBudgetConfig"),
components.KarmadaAggregatedAPIServer.PodDisruptionBudgetConfig,
components.KarmadaAggregatedAPIServer.Replicas,
)
}
if components.KubeControllerManager != nil {
checkPdb(fldPath.Child("kubeControllerManager").Child("podDisruptionBudgetConfig"),
components.KubeControllerManager.PodDisruptionBudgetConfig,
components.KubeControllerManager.Replicas,
)
}
if components.KarmadaControllerManager != nil {
checkPdb(fldPath.Child("karmadaControllerManager").Child("podDisruptionBudgetConfig"),
components.KarmadaControllerManager.PodDisruptionBudgetConfig,
components.KarmadaControllerManager.Replicas,
)
}
if components.KarmadaScheduler != nil {
checkPdb(fldPath.Child("karmadaScheduler").Child("podDisruptionBudgetConfig"),
components.KarmadaScheduler.PodDisruptionBudgetConfig,
components.KarmadaScheduler.Replicas,
)
}
if components.KarmadaDescheduler != nil {
checkPdb(fldPath.Child("karmadaDescheduler").Child("podDisruptionBudgetConfig"),
components.KarmadaDescheduler.PodDisruptionBudgetConfig,
components.KarmadaDescheduler.Replicas,
)
}
if components.KarmadaSearch != nil {
checkPdb(fldPath.Child("karmadaSearch").Child("podDisruptionBudgetConfig"),
components.KarmadaSearch.PodDisruptionBudgetConfig,
components.KarmadaSearch.Replicas,
)
}
if components.KarmadaMetricsAdapter != nil {
checkPdb(fldPath.Child("karmadaMetricsAdapter").Child("podDisruptionBudgetConfig"),
components.KarmadaMetricsAdapter.PodDisruptionBudgetConfig,
components.KarmadaMetricsAdapter.Replicas,
)
}
if components.KarmadaWebhook != nil {
checkPdb(fldPath.Child("karmadaWebhook").Child("podDisruptionBudgetConfig"),
components.KarmadaWebhook.PodDisruptionBudgetConfig,
components.KarmadaWebhook.Replicas,
)
Comment on lines +214 to +271
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

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

Missing error collection - the return value from checkPdb is not being appended to errs. All other calls to checkPdb in this function append their results to errs, but this one does not.

Suggested change
checkPdb(fldPath.Child("etcd").Child("podDisruptionBudgetConfig"),
components.Etcd.Local.PodDisruptionBudgetConfig,
components.Etcd.Local.Replicas,
)
}
if components.KarmadaAPIServer != nil {
checkPdb(fldPath.Child("karmadaAPIServer").Child("podDisruptionBudgetConfig"),
components.KarmadaAPIServer.PodDisruptionBudgetConfig,
components.KarmadaAPIServer.Replicas,
)
}
if components.KarmadaAggregatedAPIServer != nil {
checkPdb(fldPath.Child("karmadaAggregatedAPIServer").Child("podDisruptionBudgetConfig"),
components.KarmadaAggregatedAPIServer.PodDisruptionBudgetConfig,
components.KarmadaAggregatedAPIServer.Replicas,
)
}
if components.KubeControllerManager != nil {
checkPdb(fldPath.Child("kubeControllerManager").Child("podDisruptionBudgetConfig"),
components.KubeControllerManager.PodDisruptionBudgetConfig,
components.KubeControllerManager.Replicas,
)
}
if components.KarmadaControllerManager != nil {
checkPdb(fldPath.Child("karmadaControllerManager").Child("podDisruptionBudgetConfig"),
components.KarmadaControllerManager.PodDisruptionBudgetConfig,
components.KarmadaControllerManager.Replicas,
)
}
if components.KarmadaScheduler != nil {
checkPdb(fldPath.Child("karmadaScheduler").Child("podDisruptionBudgetConfig"),
components.KarmadaScheduler.PodDisruptionBudgetConfig,
components.KarmadaScheduler.Replicas,
)
}
if components.KarmadaDescheduler != nil {
checkPdb(fldPath.Child("karmadaDescheduler").Child("podDisruptionBudgetConfig"),
components.KarmadaDescheduler.PodDisruptionBudgetConfig,
components.KarmadaDescheduler.Replicas,
)
}
if components.KarmadaSearch != nil {
checkPdb(fldPath.Child("karmadaSearch").Child("podDisruptionBudgetConfig"),
components.KarmadaSearch.PodDisruptionBudgetConfig,
components.KarmadaSearch.Replicas,
)
}
if components.KarmadaMetricsAdapter != nil {
checkPdb(fldPath.Child("karmadaMetricsAdapter").Child("podDisruptionBudgetConfig"),
components.KarmadaMetricsAdapter.PodDisruptionBudgetConfig,
components.KarmadaMetricsAdapter.Replicas,
)
}
if components.KarmadaWebhook != nil {
checkPdb(fldPath.Child("karmadaWebhook").Child("podDisruptionBudgetConfig"),
components.KarmadaWebhook.PodDisruptionBudgetConfig,
components.KarmadaWebhook.Replicas,
)
errs = append(errs, checkPdb(fldPath.Child("etcd").Child("podDisruptionBudgetConfig"),
components.Etcd.Local.PodDisruptionBudgetConfig,
components.Etcd.Local.Replicas,
)...)
}
if components.KarmadaAPIServer != nil {
errs = append(errs, checkPdb(fldPath.Child("karmadaAPIServer").Child("podDisruptionBudgetConfig"),
components.KarmadaAPIServer.PodDisruptionBudgetConfig,
components.KarmadaAPIServer.Replicas,
)...)
}
if components.KarmadaAggregatedAPIServer != nil {
errs = append(errs, checkPdb(fldPath.Child("karmadaAggregatedAPIServer").Child("podDisruptionBudgetConfig"),
components.KarmadaAggregatedAPIServer.PodDisruptionBudgetConfig,
components.KarmadaAggregatedAPIServer.Replicas,
)...)
}
if components.KubeControllerManager != nil {
errs = append(errs, checkPdb(fldPath.Child("kubeControllerManager").Child("podDisruptionBudgetConfig"),
components.KubeControllerManager.PodDisruptionBudgetConfig,
components.KubeControllerManager.Replicas,
)...)
}
if components.KarmadaControllerManager != nil {
errs = append(errs, checkPdb(fldPath.Child("karmadaControllerManager").Child("podDisruptionBudgetConfig"),
components.KarmadaControllerManager.PodDisruptionBudgetConfig,
components.KarmadaControllerManager.Replicas,
)...)
}
if components.KarmadaScheduler != nil {
errs = append(errs, checkPdb(fldPath.Child("karmadaScheduler").Child("podDisruptionBudgetConfig"),
components.KarmadaScheduler.PodDisruptionBudgetConfig,
components.KarmadaScheduler.Replicas,
)...)
}
if components.KarmadaDescheduler != nil {
errs = append(errs, checkPdb(fldPath.Child("karmadaDescheduler").Child("podDisruptionBudgetConfig"),
components.KarmadaDescheduler.PodDisruptionBudgetConfig,
components.KarmadaDescheduler.Replicas,
)...)
}
if components.KarmadaSearch != nil {
errs = append(errs, checkPdb(fldPath.Child("karmadaSearch").Child("podDisruptionBudgetConfig"),
components.KarmadaSearch.PodDisruptionBudgetConfig,
components.KarmadaSearch.Replicas,
)...)
}
if components.KarmadaMetricsAdapter != nil {
errs = append(errs, checkPdb(fldPath.Child("karmadaMetricsAdapter").Child("podDisruptionBudgetConfig"),
components.KarmadaMetricsAdapter.PodDisruptionBudgetConfig,
components.KarmadaMetricsAdapter.Replicas,
)...)
}
if components.KarmadaWebhook != nil {
errs = append(errs, checkPdb(fldPath.Child("karmadaWebhook").Child("podDisruptionBudgetConfig"),
components.KarmadaWebhook.PodDisruptionBudgetConfig,
components.KarmadaWebhook.Replicas,
)...)

Copilot uses AI. Check for mistakes.
}
return errs
}

func (ctrl *Controller) validateKarmada(ctx context.Context, karmada *operatorv1alpha1.Karmada) error {
if err := validate(karmada); err != nil {
ctrl.EventRecorder.Event(karmada, corev1.EventTypeWarning, ValidationErrorReason, err.Error())
Expand Down
Loading
Loading