karmada/pkg/webhook/configuration/validating.go

170 lines
6.3 KiB
Go

package configuration
import (
"context"
"fmt"
"net/http"
"strings"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1"
)
// ValidatingAdmission validates ResourceInterpreterWebhookConfiguration 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 {
configuration := &configv1alpha1.ResourceInterpreterWebhookConfiguration{}
err := v.decoder.Decode(req, configuration)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
klog.V(2).Infof("Validating ResourceInterpreterWebhookConfiguration(%s) for request: %s", configuration.Name, req.Operation)
var allErrors field.ErrorList
hookNames := sets.NewString()
for i, hook := range configuration.Webhooks {
allErrors = append(allErrors, validateWebhook(&configuration.Webhooks[i], field.NewPath("webhooks").Index(i))...)
if hookNames.Has(hook.Name) {
allErrors = append(allErrors, field.Duplicate(field.NewPath("webhooks").Index(i).Child("name"), hook.Name))
continue
}
hookNames.Insert(hook.Name)
}
if len(allErrors) != 0 {
klog.Error(allErrors.ToAggregate())
return admission.Denied(allErrors.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
}
var supportedInterpreterOperation = sets.NewString(
string(configv1alpha1.InterpreterOperationAll),
string(configv1alpha1.InterpreterOperationInterpretReplica),
string(configv1alpha1.InterpreterOperationInterpretDependency),
string(configv1alpha1.InterpreterOperationReviseReplica),
string(configv1alpha1.InterpreterOperationRetain),
string(configv1alpha1.InterpreterOperationAggregateStatus),
string(configv1alpha1.InterpreterOperationInterpretStatus),
string(configv1alpha1.InterpreterOperationInterpretHealth),
)
var acceptedInterpreterContextVersions = []string{configv1alpha1.GroupVersion.Version}
func validateWebhook(hook *configv1alpha1.ResourceInterpreterWebhook, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
// hook.Name must be fully qualified
allErrors = append(allErrors, validation.IsFullyQualifiedDomainName(fldPath.Child("name"), hook.Name)...)
for i := range hook.Rules {
allErrors = append(allErrors, validateRuleWithOperations(&hook.Rules[i], fldPath.Child("rules").Index(i))...)
}
if hook.TimeoutSeconds != nil && (*hook.TimeoutSeconds > 30 || *hook.TimeoutSeconds < 1) {
allErrors = append(allErrors, field.Invalid(fldPath.Child("timeoutSeconds"), *hook.TimeoutSeconds, "the timeout value must be between 1 and 30 seconds"))
}
cc := hook.ClientConfig
switch {
case (cc.URL == nil) == (cc.Service == nil):
allErrors = append(allErrors, field.Required(fldPath.Child("clientConfig"), "exactly one of url or service is required"))
case cc.URL != nil:
allErrors = append(allErrors, webhook.ValidateWebhookURL(fldPath.Child("clientConfig").Child("url"), *cc.URL, true)...)
case cc.Service != nil:
allErrors = append(allErrors, webhook.ValidateWebhookService(fldPath.Child("clientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path, *cc.Service.Port)...)
}
allErrors = append(allErrors, validateInterpreterContextVersions(hook.InterpreterContextVersions, fldPath.Child("interpreterContextVersions"))...)
return allErrors
}
func hasWildcardOperation(operations []configv1alpha1.InterpreterOperation) bool {
for _, o := range operations {
if o == configv1alpha1.InterpreterOperationAll {
return true
}
}
return false
}
func validateRuleWithOperations(ruleWithOperations *configv1alpha1.RuleWithOperations, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
if len(ruleWithOperations.Operations) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("operations"), ""))
}
if len(ruleWithOperations.Operations) > 1 && hasWildcardOperation(ruleWithOperations.Operations) {
allErrors = append(allErrors, field.Invalid(fldPath.Child("operations"), ruleWithOperations.Operations, "if '*' is present, must not specify other operations"))
}
for i, operation := range ruleWithOperations.Operations {
if !supportedInterpreterOperation.Has(string(operation)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("operations").Index(i), operation, supportedInterpreterOperation.List()))
}
}
allErrors = append(allErrors, validateRule(&ruleWithOperations.Rule, fldPath)...)
return allErrors
}
func validateInterpreterContextVersions(versions []string, fldPath *field.Path) field.ErrorList {
allErrors := field.ErrorList{}
// Currently, only v1alpha1 accepted in InterpreterContextVersions
if len(versions) < 1 {
allErrors = append(allErrors, field.Required(fldPath, fmt.Sprintf("must specify one of %v", strings.Join(acceptedInterpreterContextVersions, ", "))))
} else {
visited := map[string]bool{}
hasAcceptedVersion := false
for i, v := range versions {
if visited[v] {
allErrors = append(allErrors, field.Invalid(fldPath.Index(i), v, "duplicate version"))
continue
}
visited[v] = true
for _, errString := range validation.IsDNS1035Label(v) {
allErrors = append(allErrors, field.Invalid(fldPath.Index(i), v, errString))
}
if isAcceptedExploreReviewVersions(v) {
hasAcceptedVersion = true
}
}
if !hasAcceptedVersion {
allErrors = append(allErrors, field.Invalid(
fldPath, versions,
fmt.Sprintf("must include at least one of %v", strings.Join(acceptedInterpreterContextVersions, ", "))))
}
}
return allErrors
}
func isAcceptedExploreReviewVersions(v string) bool {
for _, version := range acceptedInterpreterContextVersions {
if v == version {
return true
}
}
return false
}