karmada/pkg/webhook/multiclusterservice/validating.go

135 lines
5.2 KiB
Go

package multiclusterservice
import (
"context"
"net/http"
"strings"
admissionv1 "k8s.io/api/admission/v1"
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/util/sets"
validation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
clustervalidation "github.com/karmada-io/karmada/pkg/apis/cluster/validation"
networkingv1alpha1 "github.com/karmada-io/karmada/pkg/apis/networking/v1alpha1"
"github.com/karmada-io/karmada/pkg/util/lifted"
)
// ValidatingAdmission validates MultiClusterService object when creating/updating.
type ValidatingAdmission struct {
decoder *admission.Decoder
}
// Check if our ValidatingAdmission implements necessary interface
var _ admission.Handler = &ValidatingAdmission{}
var _ admission.DecoderInjector = &ValidatingAdmission{}
// Handle implements admission.Handler interface.
// It yields a response to an AdmissionRequest.
func (v *ValidatingAdmission) Handle(ctx context.Context, req admission.Request) admission.Response {
mcs := &networkingv1alpha1.MultiClusterService{}
err := v.decoder.Decode(req, mcs)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
klog.Infof("Validating MultiClusterService(%s/%s) for request: %s", mcs.Namespace, mcs.Name, req.Operation)
if req.Operation == admissionv1.Update {
oldMcs := &networkingv1alpha1.MultiClusterService{}
err = v.decoder.DecodeRaw(req.OldObject, oldMcs)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if errs := v.validateMCSUpdate(oldMcs, mcs); len(errs) != 0 {
klog.Errorf("Validating MultiClusterServiceUpdate failed: %v", errs)
return admission.Denied(errs.ToAggregate().Error())
}
} else {
if errs := v.validateMCS(mcs); len(errs) != 0 {
klog.Errorf("Validating MultiClusterService failed: %v", errs)
return admission.Denied(errs.ToAggregate().Error())
}
}
return admission.Allowed("")
}
// InjectDecoder implements admission.DecoderInjector interface.
// A decoder will be automatically injected.
func (v *ValidatingAdmission) InjectDecoder(d *admission.Decoder) error {
v.decoder = d
return nil
}
func (v *ValidatingAdmission) validateMCSUpdate(oldMcs, newMcs *networkingv1alpha1.MultiClusterService) field.ErrorList {
allErrs := apimachineryvalidation.ValidateObjectMetaUpdate(&newMcs.ObjectMeta, &oldMcs.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, v.validateMCS(newMcs)...)
allErrs = append(allErrs, lifted.ValidateLoadBalancerStatus(&newMcs.Status.LoadBalancer, field.NewPath("status", "loadBalancer"))...)
return allErrs
}
func (v *ValidatingAdmission) validateMCS(mcs *networkingv1alpha1.MultiClusterService) field.ErrorList {
allErrs := apimachineryvalidation.ValidateObjectMeta(&mcs.ObjectMeta, true,
apimachineryvalidation.NameIsDNS1035Label, field.NewPath("metadata"))
allErrs = append(allErrs, v.validateMultiClusterServiceSpec(mcs)...)
return allErrs
}
// validateMultiClusterServiceSpec validates MultiClusterService spec.
func (v *ValidatingAdmission) validateMultiClusterServiceSpec(mcs *networkingv1alpha1.MultiClusterService) field.ErrorList {
allErrs := field.ErrorList{}
specPath := field.NewPath("spec")
allPortNames := sets.New[string]()
portsPath := specPath.Child("ports")
for i := range mcs.Spec.Ports {
portPath := portsPath.Index(i)
port := mcs.Spec.Ports[i]
allErrs = append(allErrs, v.validateExposurePort(&port, allPortNames, portPath)...)
}
typesPath := specPath.Child("types")
for i := range mcs.Spec.Ports {
typePath := typesPath.Index(i)
exposureType := mcs.Spec.Types[i]
allErrs = append(allErrs, v.validateExposureType(&exposureType, typePath)...)
}
clusterNamesPath := specPath.Child("range").Child("clusterNames")
for i := range mcs.Spec.Range.ClusterNames {
clusterNamePath := clusterNamesPath.Index(i)
clusterName := mcs.Spec.Range.ClusterNames[i]
if errMegs := clustervalidation.ValidateClusterName(clusterName); len(errMegs) > 0 {
allErrs = append(allErrs, field.Invalid(clusterNamePath, clusterName, strings.Join(errMegs, ",")))
}
}
return allErrs
}
// validateExposurePort validates MultiClusterService ExposurePort.
func (v *ValidatingAdmission) validateExposurePort(ep *networkingv1alpha1.ExposurePort, allNames sets.Set[string], fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(ep.Name) != 0 {
allErrs = append(allErrs, lifted.ValidateDNS1123Label(ep.Name, fldPath.Child("name"))...)
if allNames.Has(ep.Name) {
allErrs = append(allErrs, field.Duplicate(fldPath.Child("name"), ep.Name))
} else {
allNames.Insert(ep.Name)
}
}
for _, msg := range validation.IsValidPortNum(int(ep.Port)) {
allErrs = append(allErrs, field.Invalid(fldPath.Child("port"), ep.Port, msg))
}
return allErrs
}
// validateExposureType validates MultiClusterService ExposureType.
func (v *ValidatingAdmission) validateExposureType(et *networkingv1alpha1.ExposureType, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if *et != networkingv1alpha1.ExposureTypeCrossCluster && *et != networkingv1alpha1.ExposureTypeLoadBalancer {
msg := "ExposureType Error"
allErrs = append(allErrs, field.Invalid(fldPath, *et, msg))
}
return allErrs
}