diff --git a/artifacts/deploy/webhook-configuration.yaml b/artifacts/deploy/webhook-configuration.yaml index b9f841bd8..642c9da66 100644 --- a/artifacts/deploy/webhook-configuration.yaml +++ b/artifacts/deploy/webhook-configuration.yaml @@ -139,6 +139,20 @@ webhooks: sideEffects: None admissionReviewVersions: ["v1"] timeoutSeconds: 10 + - name: resourceinterpretercustomization.karmada.io + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: ["config.karmada.io"] + apiVersions: ["*"] + resources: ["resourceinterpretercustomizations"] + scope: "Cluster" + clientConfig: + url: https://karmada-webhook.karmada-system.svc:443/validate-resourceinterpretercustomization + caBundle: {{caBundle}} + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: ["v1"] + timeoutSeconds: 10 - name: federatedresourcequota.karmada.io rules: - operations: ["CREATE", "UPDATE"] diff --git a/charts/karmada/templates/_karmada_webhook_configuration.tpl b/charts/karmada/templates/_karmada_webhook_configuration.tpl index 55a31aa89..4f8b0709f 100644 --- a/charts/karmada/templates/_karmada_webhook_configuration.tpl +++ b/charts/karmada/templates/_karmada_webhook_configuration.tpl @@ -143,4 +143,18 @@ webhooks: sideEffects: None admissionReviewVersions: ["v1"] timeoutSeconds: 3 + - name: resourceinterpretercustomization.karmada.io + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: ["config.karmada.io"] + apiVersions: ["*"] + resources: ["resourceinterpretercustomizations"] + scope: "Cluster" + clientConfig: + url: https://{{ $name }}-webhook.{{ $namespace }}.svc:443/validate-resourceinterpretercustomization + {{- include "karmada.webhook.caBundle" . | nindent 6 }} + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: ["v1"] + timeoutSeconds: 3 {{- end -}} diff --git a/cmd/webhook/app/webhook.go b/cmd/webhook/app/webhook.go index 2915ec247..7149ffee0 100644 --- a/cmd/webhook/app/webhook.go +++ b/cmd/webhook/app/webhook.go @@ -28,6 +28,7 @@ import ( "github.com/karmada-io/karmada/pkg/webhook/federatedresourcequota" "github.com/karmada-io/karmada/pkg/webhook/overridepolicy" "github.com/karmada-io/karmada/pkg/webhook/propagationpolicy" + "github.com/karmada-io/karmada/pkg/webhook/resourceinterpretercustomization" "github.com/karmada-io/karmada/pkg/webhook/work" ) @@ -125,6 +126,7 @@ func Run(ctx context.Context, opts *options.Options) error { hookServer.Register("/convert", &conversion.Webhook{}) hookServer.Register("/validate-resourceinterpreterwebhookconfiguration", &webhook.Admission{Handler: &configuration.ValidatingAdmission{}}) hookServer.Register("/validate-federatedresourcequota", &webhook.Admission{Handler: &federatedresourcequota.ValidatingAdmission{}}) + hookServer.Register("/validate-resourceinterpretercustomization", &webhook.Admission{Handler: &resourceinterpretercustomization.ValidatingAdmission{Client: hookManager.GetClient()}}) hookServer.WebhookMux.Handle("/readyz/", http.StripPrefix("/readyz/", &healthz.Handler{})) // blocks until the context is done. diff --git a/pkg/karmadactl/cmdinit/karmada/webhook_configuration.go b/pkg/karmadactl/cmdinit/karmada/webhook_configuration.go index 85aa7b9f4..fe3a8d608 100644 --- a/pkg/karmadactl/cmdinit/karmada/webhook_configuration.go +++ b/pkg/karmadactl/cmdinit/karmada/webhook_configuration.go @@ -157,7 +157,22 @@ webhooks: failurePolicy: Fail sideEffects: None admissionReviewVersions: ["v1"] - timeoutSeconds: 3`, systemNamespace, caBundle) + timeoutSeconds: 3 + - name: resourceinterpretercustomization.karmada.io + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: ["config.karmada.io"] + apiVersions: ["*"] + resources: ["resourceexploringwebhookconfigurations"] + scope: "Cluster" + clientConfig: + url: https://karmada-webhook.%[1]s.svc:443/validate-resourceinterpretercustomization + caBundle: %[2]s + failurePolicy: Fail + sideEffects: None + admissionReviewVersions: ["v1"] + timeoutSeconds: 3 +`, systemNamespace, caBundle) } func createOrUpdateValidatingWebhookConfiguration(c kubernetes.Interface, staticYaml string) error { diff --git a/pkg/karmadactl/interpret/execute.go b/pkg/karmadactl/interpret/execute.go index d8aab86b7..14e4cf371 100644 --- a/pkg/karmadactl/interpret/execute.go +++ b/pkg/karmadactl/interpret/execute.go @@ -13,6 +13,7 @@ import ( workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" "github.com/karmada-io/karmada/pkg/karmadactl/util/genericresource" "github.com/karmada-io/karmada/pkg/resourceinterpreter/configurableinterpreter" + "github.com/karmada-io/karmada/pkg/util/interpreter" ) func (o *Options) completeExecute(f util.Factory) []error { @@ -72,7 +73,7 @@ func (o *Options) runExecute() error { return fmt.Errorf("fail to get status items: %v", err) } - args := ruleArgs{ + args := interpreter.RuleArgs{ Desired: desired, Observed: observed, Status: status, @@ -92,7 +93,7 @@ func (o *Options) runExecute() error { return nil } -func printExecuteResult(w, errOut io.Writer, name string, result *ruleResult) { +func printExecuteResult(w, errOut io.Writer, name string, result *interpreter.RuleResult) { if result.Err != nil { fmt.Fprintf(errOut, "Execute %s error: %v\n", name, result.Err) return diff --git a/pkg/karmadactl/interpret/interpret.go b/pkg/karmadactl/interpret/interpret.go index 21223c414..1578e1b60 100644 --- a/pkg/karmadactl/interpret/interpret.go +++ b/pkg/karmadactl/interpret/interpret.go @@ -20,6 +20,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/util/genericresource" "github.com/karmada-io/karmada/pkg/util/gclient" "github.com/karmada-io/karmada/pkg/util/helper" + "github.com/karmada-io/karmada/pkg/util/interpreter" ) var ( @@ -72,7 +73,7 @@ const ( func NewCmdInterpret(f util.Factory, parentCommand string, streams genericclioptions.IOStreams) *cobra.Command { o := &Options{ IOStreams: streams, - Rules: allRules, + Rules: interpreter.AllResourceInterpreterCustomizationRules, } cmd := &cobra.Command{ Use: "interpret (-f FILENAME) (--operation OPERATION) [--ARGS VALUE]... ", @@ -123,7 +124,7 @@ type Options struct { ObservedResult *resource.Result StatusResult *genericresource.Result - Rules Rules + Rules interpreter.Rules genericclioptions.IOStreams } diff --git a/pkg/util/interpreter/rules.go b/pkg/util/interpreter/matcher.go similarity index 100% rename from pkg/util/interpreter/rules.go rename to pkg/util/interpreter/matcher.go diff --git a/pkg/util/interpreter/rules_test.go b/pkg/util/interpreter/matcher_test.go similarity index 100% rename from pkg/util/interpreter/rules_test.go rename to pkg/util/interpreter/matcher_test.go diff --git a/pkg/karmadactl/interpret/rule.go b/pkg/util/interpreter/rule.go similarity index 89% rename from pkg/karmadactl/interpret/rule.go rename to pkg/util/interpreter/rule.go index b439a50b3..f151c4408 100644 --- a/pkg/karmadactl/interpret/rule.go +++ b/pkg/util/interpreter/rule.go @@ -1,4 +1,4 @@ -package interpret +package interpreter import ( "fmt" @@ -11,7 +11,8 @@ import ( "github.com/karmada-io/karmada/pkg/resourceinterpreter/configurableinterpreter" ) -var allRules = []Rule{ +// AllResourceInterpreterCustomizationRules all InterpreterOperations +var AllResourceInterpreterCustomizationRules = []Rule{ &retentionRule{}, &replicaResourceRule{}, &replicaRevisionRule{}, @@ -46,7 +47,7 @@ func (r *retentionRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomiza c.Spec.Customizations.Retention.LuaScript = script } -func (r *retentionRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { +func (r *retentionRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args RuleArgs) *RuleResult { desired, err := args.getDesiredObjectOrError() if err != nil { return newRuleResultWithError(err) @@ -91,7 +92,7 @@ func (r *replicaResourceRule) SetScript(c *configv1alpha1.ResourceInterpreterCus c.Spec.Customizations.ReplicaResource.LuaScript = script } -func (r *replicaResourceRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { +func (r *replicaResourceRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args RuleArgs) *RuleResult { obj, err := args.getObjectOrError() if err != nil { return newRuleResultWithError(err) @@ -132,7 +133,7 @@ func (r *replicaRevisionRule) SetScript(c *configv1alpha1.ResourceInterpreterCus c.Spec.Customizations.ReplicaRevision.LuaScript = script } -func (r *replicaRevisionRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { +func (r *replicaRevisionRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args RuleArgs) *RuleResult { obj, err := args.getObjectOrError() if err != nil { return newRuleResultWithError(err) @@ -173,7 +174,7 @@ func (s *statusReflectionRule) SetScript(c *configv1alpha1.ResourceInterpreterCu c.Spec.Customizations.StatusReflection.LuaScript = script } -func (s *statusReflectionRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { +func (s *statusReflectionRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args RuleArgs) *RuleResult { obj, err := args.getObjectOrError() if err != nil { return newRuleResultWithError(err) @@ -214,7 +215,7 @@ func (s *statusAggregationRule) SetScript(c *configv1alpha1.ResourceInterpreterC c.Spec.Customizations.StatusAggregation.LuaScript = script } -func (s *statusAggregationRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { +func (s *statusAggregationRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args RuleArgs) *RuleResult { obj, err := args.getObjectOrError() if err != nil { return newRuleResultWithError(err) @@ -260,7 +261,7 @@ func (h *healthInterpretationRule) SetScript(c *configv1alpha1.ResourceInterpret c.Spec.Customizations.HealthInterpretation.LuaScript = script } -func (h *healthInterpretationRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { +func (h *healthInterpretationRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args RuleArgs) *RuleResult { obj, err := args.getObjectOrError() if err != nil { return newRuleResultWithError(err) @@ -301,7 +302,7 @@ func (d *dependencyInterpretationRule) SetScript(c *configv1alpha1.ResourceInter c.Spec.Customizations.DependencyInterpretation.LuaScript = script } -func (d *dependencyInterpretationRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { +func (d *dependencyInterpretationRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args RuleArgs) *RuleResult { obj, err := args.getObjectOrError() if err != nil { return newRuleResultWithError(err) @@ -325,7 +326,7 @@ type Rule interface { // SetScript set the script for the rule. If script is empty, disable the rule. SetScript(*configv1alpha1.ResourceInterpreterCustomization, string) // Run execute the rule with given args, and return the result. - Run(*configurableinterpreter.ConfigurableInterpreter, ruleArgs) *ruleResult + Run(*configurableinterpreter.ConfigurableInterpreter, RuleArgs) *RuleResult } // Rules is a series of rules. @@ -365,28 +366,29 @@ func (r Rules) Get(name string) Rule { return nil } -type ruleArgs struct { +// RuleArgs rule execution args. +type RuleArgs struct { Desired *unstructured.Unstructured Observed *unstructured.Unstructured Status []workv1alpha2.AggregatedStatusItem Replica int64 } -func (r ruleArgs) getDesiredObjectOrError() (*unstructured.Unstructured, error) { +func (r RuleArgs) getDesiredObjectOrError() (*unstructured.Unstructured, error) { if r.Desired == nil { return nil, fmt.Errorf("desired, desired-file options are not set") } return r.Desired, nil } -func (r ruleArgs) getObservedObjectOrError() (*unstructured.Unstructured, error) { +func (r RuleArgs) getObservedObjectOrError() (*unstructured.Unstructured, error) { if r.Observed == nil { return nil, fmt.Errorf("observed, observed-file options are not set") } return r.Observed, nil } -func (r ruleArgs) getObjectOrError() (*unstructured.Unstructured, error) { +func (r RuleArgs) getObjectOrError() (*unstructured.Unstructured, error) { if r.Desired == nil && r.Observed == nil { return nil, fmt.Errorf("desired-file, observed-file options are not set") } @@ -399,27 +401,29 @@ func (r ruleArgs) getObjectOrError() (*unstructured.Unstructured, error) { return r.Observed, nil } -type nameValue struct { +// NameValue name and value. +type NameValue struct { Name string Value interface{} } -type ruleResult struct { - Results []nameValue +// RuleResult rule execution result. +type RuleResult struct { + Results []NameValue Err error } -func newRuleResult() *ruleResult { - return &ruleResult{} +func newRuleResult() *RuleResult { + return &RuleResult{} } -func newRuleResultWithError(err error) *ruleResult { - return &ruleResult{ +func newRuleResultWithError(err error) *RuleResult { + return &RuleResult{ Err: err, } } -func (r *ruleResult) add(name string, value interface{}) *ruleResult { - r.Results = append(r.Results, nameValue{Name: name, Value: value}) +func (r *RuleResult) add(name string, value interface{}) *RuleResult { + r.Results = append(r.Results, NameValue{Name: name, Value: value}) return r } diff --git a/pkg/webhook/resourceinterpretercustomization/helper.go b/pkg/webhook/resourceinterpretercustomization/helper.go new file mode 100644 index 000000000..e9637001e --- /dev/null +++ b/pkg/webhook/resourceinterpretercustomization/helper.go @@ -0,0 +1,58 @@ +package resourceinterpretercustomization + +import ( + "context" + "fmt" + "time" + + configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" + "github.com/karmada-io/karmada/pkg/resourceinterpreter/configurableinterpreter/luavm" + "github.com/karmada-io/karmada/pkg/util/interpreter" +) + +func validateCustomizationRule(oldRules, newRules *configv1alpha1.ResourceInterpreterCustomization) error { + if oldRules.Spec.Target.APIVersion != newRules.Spec.Target.APIVersion || + oldRules.Spec.Target.Kind != newRules.Spec.Target.Kind { + return nil + } + for _, rule := range interpreter.AllResourceInterpreterCustomizationRules { + oldScript := rule.GetScript(oldRules) + newScript := rule.GetScript(newRules) + if oldScript != "" && newScript != "" { + return fmt.Errorf("conflicting with InterpreterOperation(%s) of existing ResourceInterpreterCustomization(%s)", rule.Name(), oldRules.Name) + } + } + return nil +} + +func checkCustomizationsRule(customization *configv1alpha1.ResourceInterpreterCustomization) error { + ctx, cancel := context.WithTimeout(context.TODO(), time.Second) + defer cancel() + l, err := luavm.NewWithContext(ctx) + if err != nil { + return err + } + defer l.Close() + for _, rule := range interpreter.AllResourceInterpreterCustomizationRules { + if script := rule.GetScript(customization); script != "" { + if _, err = l.LoadString(script); err != nil { + return fmt.Errorf("InterpreterOperation(%s) Lua script error: %v", rule.Name(), err) + } + } + } + return nil +} + +func validateResourceInterpreterCustomizations(newConfig *configv1alpha1.ResourceInterpreterCustomization, customizations *configv1alpha1.ResourceInterpreterCustomizationList) error { + for _, config := range customizations.Items { + // skip self verification + if config.Name == newConfig.Name { + continue + } + oldConfig := config + if err := validateCustomizationRule(&oldConfig, newConfig); err != nil { + return err + } + } + return checkCustomizationsRule(newConfig) +} diff --git a/pkg/webhook/resourceinterpretercustomization/helper_test.go b/pkg/webhook/resourceinterpretercustomization/helper_test.go new file mode 100644 index 000000000..03e0d46f0 --- /dev/null +++ b/pkg/webhook/resourceinterpretercustomization/helper_test.go @@ -0,0 +1,477 @@ +package resourceinterpretercustomization + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" +) + +func Test_validateCustomizationRule(t *testing.T) { + type args struct { + oldRules *configv1alpha1.ResourceInterpreterCustomization + newRules *configv1alpha1.ResourceInterpreterCustomization + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "the different Kind of ResourceInterpreterCustomization", + args: args{ + oldRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + }, + }, + newRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "bar", + }, + }, + }, + }, + wantErr: false, + }, + { + name: " the different APIVersion of ResourceInterpreterCustomization", + args: args{ + oldRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + }, + }, + newRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v2", + Kind: "kind", + }, + }, + }, + }, + wantErr: false, + }, + { + name: "the same InterpreterOperation(Retention) of ResourceInterpreterCustomization", + args: args{ + oldRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + Retention: &configv1alpha1.LocalValueRetention{LuaScript: "LuaScript"}, + }, + }, + }, + newRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + Retention: &configv1alpha1.LocalValueRetention{LuaScript: "LuaScript"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "the same InterpreterOperation(ReplicaResource) of ResourceInterpreterCustomization", + args: args{ + oldRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + ReplicaResource: &configv1alpha1.ReplicaResourceRequirement{LuaScript: "LuaScript"}, + }, + }, + }, + newRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + ReplicaResource: &configv1alpha1.ReplicaResourceRequirement{LuaScript: "LuaScript"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "the same InterpreterOperation(ReplicaRevision) of ResourceInterpreterCustomization", + args: args{ + oldRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + ReplicaRevision: &configv1alpha1.ReplicaRevision{LuaScript: "LuaScript"}, + }, + }, + }, + newRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + ReplicaRevision: &configv1alpha1.ReplicaRevision{LuaScript: "LuaScript"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "the same InterpreterOperation(StatusReflection) of ResourceInterpreterCustomization", + args: args{ + oldRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + StatusReflection: &configv1alpha1.StatusReflection{LuaScript: "LuaScript"}, + }, + }, + }, + newRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + StatusReflection: &configv1alpha1.StatusReflection{LuaScript: "LuaScript"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "the same InterpreterOperation(StatusAggregation) of ResourceInterpreterCustomization", + args: args{ + oldRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + StatusAggregation: &configv1alpha1.StatusAggregation{LuaScript: "LuaScript"}, + }, + }, + }, + newRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + StatusAggregation: &configv1alpha1.StatusAggregation{LuaScript: "LuaScript"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "the same InterpreterOperation(HealthInterpretation) of ResourceInterpreterCustomization", + args: args{ + oldRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + HealthInterpretation: &configv1alpha1.HealthInterpretation{LuaScript: "LuaScript"}, + }, + }, + }, + newRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + HealthInterpretation: &configv1alpha1.HealthInterpretation{LuaScript: "LuaScript"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "the same InterpreterOperation(DependencyInterpretation) of ResourceInterpreterCustomization", + args: args{ + oldRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + DependencyInterpretation: &configv1alpha1.DependencyInterpretation{LuaScript: "LuaScript"}, + }, + }, + }, + newRules: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, + Customizations: configv1alpha1.CustomizationRules{ + DependencyInterpretation: &configv1alpha1.DependencyInterpretation{LuaScript: "LuaScript"}, + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateCustomizationRule(tt.args.oldRules, tt.args.newRules); (err != nil) != tt.wantErr { + t.Errorf("validateCustomizationRule() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_validateResourceInterpreterCustomizations(t *testing.T) { + type args struct { + customization *configv1alpha1.ResourceInterpreterCustomization + customizations *configv1alpha1.ResourceInterpreterCustomizationList + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "the same name of ResourceInterpreterCustomization", + args: args{ + customization: &configv1alpha1.ResourceInterpreterCustomization{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, Customizations: configv1alpha1.CustomizationRules{Retention: &configv1alpha1.LocalValueRetention{LuaScript: `function Retain(desiredObj, observedObj) end`}}}}, + customizations: &configv1alpha1.ResourceInterpreterCustomizationList{ + Items: []configv1alpha1.ResourceInterpreterCustomization{ + {ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{Target: configv1alpha1.CustomizationTarget{ + APIVersion: "foo/v1", + Kind: "kind", + }, Customizations: configv1alpha1.CustomizationRules{Retention: &configv1alpha1.LocalValueRetention{LuaScript: "function Retain(desiredObj, observedObj) end"}}}}}}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateResourceInterpreterCustomizations(tt.args.customization, tt.args.customizations); (err != nil) != tt.wantErr { + t.Errorf("validateResourceInterpreterCustomizations() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_checkCustomizationsRule(t *testing.T) { + type args struct { + customization *configv1alpha1.ResourceInterpreterCustomization + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "correct lua script", + args: args{customization: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Customizations: configv1alpha1.CustomizationRules{ + Retention: &configv1alpha1.LocalValueRetention{LuaScript: ` +function Retain(desiredObj, observedObj) + desiredObj.spec.fieldFoo = observedObj.spec.fieldFoo + return desiredObj +end +`}, + ReplicaResource: &configv1alpha1.ReplicaResourceRequirement{LuaScript: ` +function GetReplicas(desiredObj) + nodeClaim = {} + resourceRequest = {} + result = {} + + result.replica = desiredObj.spec.replicas + result.resourceRequest = desiredObj.spec.template.spec.containers[0].resources.limits + + nodeClaim.nodeSelector = desiredObj.spec.template.spec.nodeSelector + nodeClaim.tolerations = desiredObj.spec.template.spec.tolerations + result.nodeClaim = nodeClaim + + return result +end +`}, + ReplicaRevision: &configv1alpha1.ReplicaRevision{LuaScript: ` +function ReviseReplica(desiredObj, desiredReplica) + desiredObj.spec.replicas = desiredReplica + return desiredObj +end +`}, + StatusReflection: &configv1alpha1.StatusReflection{LuaScript: ` +function ReflectStatus(observedObj) + status = {} + status.readyReplicas = observedObj.status.observedObj + return status +end + +`}, + StatusAggregation: &configv1alpha1.StatusAggregation{LuaScript: ` +function AggregateStatus(desiredObj, statusItems) + for i = 1, #items do + desiredObj.status.readyReplicas = desiredObj.status.readyReplicas + items[i].readyReplicas + end + return desiredObj +end + +`}, + HealthInterpretation: &configv1alpha1.HealthInterpretation{LuaScript: ` +function InterpretHealth(observedObj) + if observedObj.status.readyReplicas == observedObj.spec.replicas then + return true + end +end +`}, + DependencyInterpretation: &configv1alpha1.DependencyInterpretation{LuaScript: ` +function GetDependencies(desiredObj) + dependencies = {} + if desiredObj.spec.serviceAccountName ~= "" and desiredObj.spec.serviceAccountName ~= "default" then + dependency = {} + dependency.apiVersion = "v1" + dependency.kind = "ServiceAccount" + dependency.name = desiredObj.spec.serviceAccountName + dependency.namespace = desiredObj.namespace + dependencies[0] = {} + dependencies[0] = dependency + end + return dependencies +end + +`}}, + }, + }, + }, + wantErr: false, + }, + { + name: "Retention contains the wrong lua script", + args: args{customization: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Customizations: configv1alpha1.CustomizationRules{ + Retention: &configv1alpha1.LocalValueRetention{LuaScript: `function Retain(desiredObj, observedObj)`}, + }, + }, + }}, + wantErr: true, + }, + { + name: "ReplicaResource contains the wrong lua script", + args: args{customization: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Customizations: configv1alpha1.CustomizationRules{ + ReplicaResource: &configv1alpha1.ReplicaResourceRequirement{LuaScript: `function GetReplicas(desiredObj)`}, + }, + }, + }}, + wantErr: true, + }, + { + name: "ReplicaRevision contains the wrong lua script", + args: args{customization: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Customizations: configv1alpha1.CustomizationRules{ + ReplicaRevision: &configv1alpha1.ReplicaRevision{LuaScript: `function ReviseReplica(desiredObj, desiredReplica)`}}, + }, + }}, + wantErr: true, + }, + { + name: "StatusReflection contains the wrong lua script", + args: args{customization: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Customizations: configv1alpha1.CustomizationRules{ + StatusReflection: &configv1alpha1.StatusReflection{LuaScript: `function ReflectStatus(observedObj)`}}, + }, + }}, + wantErr: true, + }, + { + name: "StatusAggregation contains the wrong lua script", + args: args{customization: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Customizations: configv1alpha1.CustomizationRules{ + StatusAggregation: &configv1alpha1.StatusAggregation{LuaScript: `function AggregateStatus(desiredObj, statusItems)`}}, + }, + }}, + wantErr: true, + }, + { + name: "HealthInterpretation contains the wrong lua script", + args: args{customization: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Customizations: configv1alpha1.CustomizationRules{ + HealthInterpretation: &configv1alpha1.HealthInterpretation{LuaScript: `function InterpretHealth(observedObj)`}}, + }, + }}, + wantErr: true, + }, + { + name: "DependencyInterpretation contains the wrong lua script", + args: args{customization: &configv1alpha1.ResourceInterpreterCustomization{ + Spec: configv1alpha1.ResourceInterpreterCustomizationSpec{ + Customizations: configv1alpha1.CustomizationRules{ + DependencyInterpretation: &configv1alpha1.DependencyInterpretation{LuaScript: `function GetDependencies(desiredObj)`}}, + }, + }}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := checkCustomizationsRule(tt.args.customization); (err != nil) != tt.wantErr { + t.Errorf("checkCustomizationsRule() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/webhook/resourceinterpretercustomization/validating.go b/pkg/webhook/resourceinterpretercustomization/validating.go new file mode 100644 index 000000000..b9212d1bf --- /dev/null +++ b/pkg/webhook/resourceinterpretercustomization/validating.go @@ -0,0 +1,49 @@ +package resourceinterpretercustomization + +import ( + "context" + "net/http" + + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" +) + +// Check if our ValidatingAdmission implements necessary interface +var _ admission.Handler = &ValidatingAdmission{} +var _ admission.DecoderInjector = &ValidatingAdmission{} + +// ValidatingAdmission validates ResourceInterpreterCustomization object when creating/updating. +type ValidatingAdmission struct { + client.Client + decoder *admission.Decoder +} + +// 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.ResourceInterpreterCustomization{} + + err := v.decoder.Decode(req, configuration) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + klog.V(2).Infof("Validating ResourceInterpreterCustomization(%s) for request: %s", configuration.Name, req.Operation) + configs := &configv1alpha1.ResourceInterpreterCustomizationList{} + if err = v.List(ctx, configs); err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + if err = validateResourceInterpreterCustomizations(configuration, configs); err != nil { + return admission.Denied(err.Error()) + } + return admission.Allowed("") +} + +// InjectDecoder implements admission.DecoderInjector interface. +// A decoder will be automatically injected. +func (v *ValidatingAdmission) InjectDecoder(decoder *admission.Decoder) error { + v.decoder = decoder + return nil +}