Implement ResourceInterpreterCustomization ValidatingAdmission

Signed-off-by: chaunceyjiang <chaunceyjiang@gmail.com>
This commit is contained in:
chaunceyjiang 2022-11-08 12:44:37 +08:00
parent eb3763c201
commit 0c16431a51
12 changed files with 663 additions and 28 deletions

View File

@ -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"]

View File

@ -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 -}}

View File

@ -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.

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View File

@ -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
}