diff --git a/pkg/karmadactl/interpret/check.go b/pkg/karmadactl/interpret/check.go new file mode 100644 index 000000000000..81f576511923 --- /dev/null +++ b/pkg/karmadactl/interpret/check.go @@ -0,0 +1,93 @@ +package interpret + +import ( + "context" + "fmt" + "strings" + "time" + + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/cli-runtime/pkg/resource" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/karmada-io/karmada/pkg/resourceinterpreter/configurableinterpreter/luavm" +) + +func (o *Options) runCheck() error { + w := printers.GetNewTabWriter(o.Out) + defer w.Flush() + + failed := false + + err := o.CustomizationResult.Visit(func(info *resource.Info, _ error) error { + var visitErr error + fmt.Fprintln(w, "-----------------------------------") + + source := info.Source + if info.Name != "" { + source = info.Name + } + fmt.Fprintf(w, "SOURCE: %s\n", source) + + customization, visitErr := asResourceInterpreterCustomization(info.Object) + if visitErr != nil { + failed = true + fmt.Fprintf(w, "%v\n", visitErr) + return nil + } + + kind := customization.Spec.Target.Kind + if kind == "" { + failed = true + fmt.Fprintln(w, "target.kind no set") + return nil + } + apiVersion := customization.Spec.Target.APIVersion + if apiVersion == "" { + failed = true + fmt.Fprintln(w, "target.apiVersion no set") + return nil + } + + fmt.Fprintf(w, "TARGET: %s %s\t\n", apiVersion, kind) + fmt.Fprintf(w, "RULERS:\n") + for _, r := range o.Rules { + fmt.Fprintf(w, " %s:\t", r.Name()) + + script := r.GetScript(customization) + if script == "" { + fmt.Fprintln(w, "UNSET") + continue + } + checkErr := checkScrip(script) + if checkErr != nil { + failed = true + fmt.Fprintf(w, "%s: %s\t\n", "ERROR", strings.TrimSpace(checkErr.Error())) + continue + } + + fmt.Fprintln(w, "PASS") + } + return nil + }) + if err != nil { + return err + } + if failed { + // As failed infos are printed above. So don't print it again. + return cmdutil.ErrExit + } + return nil +} + +func checkScrip(script string) error { + ctx, cancel := context.WithTimeout(context.TODO(), time.Second) + defer cancel() + l, err := luavm.NewWithContext(ctx) + if err != nil { + return err + } + defer l.Close() + _, err = l.LoadString(script) + return err +} diff --git a/pkg/karmadactl/interpret/execute.go b/pkg/karmadactl/interpret/execute.go new file mode 100644 index 000000000000..ea8b25594851 --- /dev/null +++ b/pkg/karmadactl/interpret/execute.go @@ -0,0 +1,16 @@ +package interpret + +import ( + "fmt" + + "github.com/spf13/cobra" + "k8s.io/kubectl/pkg/cmd/util" +) + +func (o *Options) completeExecute(_ util.Factory, _ *cobra.Command, _ []string) []error { + return nil +} + +func (o *Options) runExecute() error { + return fmt.Errorf("not implement") +} diff --git a/pkg/karmadactl/interpret/interpret.go b/pkg/karmadactl/interpret/interpret.go new file mode 100644 index 000000000000..12360c22b5f4 --- /dev/null +++ b/pkg/karmadactl/interpret/interpret.go @@ -0,0 +1,150 @@ +package interpret + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" + "github.com/karmada-io/karmada/pkg/karmadactl/options" + "github.com/karmada-io/karmada/pkg/karmadactl/util" + "github.com/karmada-io/karmada/pkg/util/gclient" +) + +var ( + interpretLong = templates.LongDesc(` + Validate and test interpreter customization before applying it to the control plane. + + 1. Validate the ResourceInterpreterCustomization configuration as per API schema + and try to load the scripts for syntax check. + + 2. Run the rules locally and test if the result is expected. Similar to the dry run. + +`) + + interpretExample = templates.Examples(` + # Check the customizations in file + %[1]s interpret -f customization.json --check + # Execute the retention rule for + %[1]s interpret -f customization.yml --operation retain --desired-file desired.yml --observed-file observed.yml + # Execute the replicaRevision rule for + %[1]s interpret -f customization.yml --operation reviseReplica --observed-file observed.yml --desired-replica 2 + # Execute the statusReflection rule for + %[1]s interpret -f customization.yml --operation interpretStatus --observed-file observed.yml + # Execute the healthInterpretation rule + %[1]s interpret -f customization.yml --operation interpretHealth --observed-file observed.yml + # Execute the dependencyInterpretation rule + %[1]s interpret -f customization.yml --operation interpretDependency --observed-file observed.yml + # Execute the statusAggregation rule + %[1]s interpret -f customization.yml --operation aggregateStatus --status-file status1.yml --status-file status2.yml + +`) +) + +const ( + customizationResourceName = "resourceinterpretercustomizations" +) + +// NewCmdInterpret new interpret command. +func NewCmdInterpret(f util.Factory, parentCommand string, streams genericclioptions.IOStreams) *cobra.Command { + o := &Options{ + IOStreams: streams, + Rules: allRules, + } + cmd := &cobra.Command{ + Use: "interpret (-f FILENAME) (--operation OPERATION) [--ARGS VALUE]... ", + Short: "Validate and test interpreter customization before applying it to the control plane", + Long: interpretLong, + SilenceUsage: true, + DisableFlagsInUseLine: true, + Example: fmt.Sprintf(interpretExample, parentCommand), + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + Annotations: map[string]string{ + util.TagCommandGroup: util.GroupClusterTroubleshootingAndDebugging, + }, + } + + flags := cmd.Flags() + options.AddKubeConfigFlags(flags) + flags.StringVar(&o.Operation, "operation", o.Operation, "The interpret operation to use. One of: ("+strings.Join(o.Rules.Names(), ",")+")") + flags.BoolVar(&o.Check, "check", false, "Validates the given ResourceInterpreterCustomization configuration(s)") + flags.StringVar(&o.DesiredFile, "desired-file", o.DesiredFile, "Filename, directory, or URL to files identifying the resource to use as desiredObj argument in rule script.") + flags.StringVar(&o.ObservedFile, "observed-file", o.ObservedFile, "Filename, directory, or URL to files identifying the resource to use as observedObj argument in rule script.") + flags.StringSliceVar(&o.StatusFile, "status-file", o.StatusFile, "Filename, directory, or URL to files identifying the resource to use as statusItems argument in rule script.") + flags.Int32Var(&o.DesiredReplica, "desired-replica", o.DesiredReplica, "The desiredReplica argument in rule script.") + cmdutil.AddJsonFilenameFlag(flags, &o.FilenameOptions.Filenames, "Filename, directory, or URL to files containing the customizations") + flags.BoolVarP(&o.FilenameOptions.Recursive, "recursive", "R", false, "Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory.") + + return cmd +} + +// Options contains the input to the interpret command. +type Options struct { + resource.FilenameOptions + + Operation string + Check bool + + // args + DesiredFile string + ObservedFile string + StatusFile []string + DesiredReplica int32 + + CustomizationResult *resource.Result + + Rules Rules + + genericclioptions.IOStreams +} + +// Complete ensures that options are valid and marshals them if necessary +func (o *Options) Complete(f util.Factory, cmd *cobra.Command, args []string) error { + scheme := gclient.NewSchema() + o.CustomizationResult = f.NewBuilder(). + WithScheme(scheme, scheme.PrioritizedVersionsAllGroups()...). + FilenameParam(false, &o.FilenameOptions). + ResourceNames(customizationResourceName, args...). + RequireObject(true). + Local(). + Do() + + var errs []error + errs = append(errs, o.CustomizationResult.Err()) + errs = append(errs, o.completeExecute(f, cmd, args)...) + return errors.NewAggregate(errs) +} + +// Validate checks the EditOptions to see if there is sufficient information to run the command. +func (o *Options) Validate() error { + return nil +} + +// Run describe information of resources +func (o *Options) Run() error { + switch { + case o.Check: + return o.runCheck() + default: + return o.runExecute() + } +} + +func asResourceInterpreterCustomization(o runtime.Object) (*configv1alpha1.ResourceInterpreterCustomization, error) { + c, ok := o.(*configv1alpha1.ResourceInterpreterCustomization) + if !ok { + return nil, fmt.Errorf("not a ResourceInterpreterCustomization: %#v", o) + } + return c, nil +} diff --git a/pkg/karmadactl/interpret/rule.go b/pkg/karmadactl/interpret/rule.go new file mode 100644 index 000000000000..a6f80b1e21f8 --- /dev/null +++ b/pkg/karmadactl/interpret/rule.go @@ -0,0 +1,404 @@ +package interpret + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" + workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" + "github.com/karmada-io/karmada/pkg/resourceinterpreter/configurableinterpreter" +) + +var allRules = []Rule{ + &retentionRule{}, + &replicaResourceRule{}, + &replicaRevisionRule{}, + &statusReflectionRule{}, + &statusAggregationRule{}, + &healthInterpretationRule{}, + &dependencyInterpretationRule{}, +} + +type retentionRule struct{} + +func (r *retentionRule) Name() string { + return string(configv1alpha1.InterpreterOperationRetain) +} + +func (r *retentionRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { + if c.Spec.Customizations.Retention != nil { + return c.Spec.Customizations.Retention.LuaScript + } + return "" +} + +func (r *retentionRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) { + if script == "" { + c.Spec.Customizations.Retention = nil + return + } + + if c.Spec.Customizations.Retention == nil { + c.Spec.Customizations.Retention = &configv1alpha1.LocalValueRetention{} + } + c.Spec.Customizations.Retention.LuaScript = script +} + +func (r *retentionRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { + desired, err := args.getDesiredObjectOrError() + if err != nil { + return newRuleResultWithError(err) + } + observed, err := args.getObservedObjectOrError() + if err != nil { + return newRuleResultWithError(err) + } + retained, enabled, err := interpreter.Retain(desired, observed) + if err != nil { + return newRuleResultWithError(err) + } + if !enabled { + return newRuleResultWithError(fmt.Errorf("rule is not enabled")) + } + return newRuleResult().add("retained", retained) +} + +type replicaResourceRule struct { +} + +func (r *replicaResourceRule) Name() string { + return string(configv1alpha1.InterpreterOperationInterpretReplica) +} + +func (r *replicaResourceRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { + if c.Spec.Customizations.ReplicaResource != nil { + return c.Spec.Customizations.ReplicaResource.LuaScript + } + return "" +} + +func (r *replicaResourceRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) { + if script == "" { + c.Spec.Customizations.ReplicaResource = nil + return + } + + if c.Spec.Customizations.ReplicaResource == nil { + c.Spec.Customizations.ReplicaResource = &configv1alpha1.ReplicaResourceRequirement{} + } + c.Spec.Customizations.ReplicaResource.LuaScript = script +} + +func (r *replicaResourceRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { + obj, err := args.getObjectOrError() + if err != nil { + return newRuleResultWithError(err) + } + replica, requires, enabled, err := interpreter.GetReplicas(obj) + if err != nil { + return newRuleResultWithError(err) + } + if !enabled { + return newRuleResultWithError(fmt.Errorf("rule is not enabled")) + } + return newRuleResult().add("replica", replica).add("requires", requires) +} + +type replicaRevisionRule struct { +} + +func (r *replicaRevisionRule) Name() string { + return string(configv1alpha1.InterpreterOperationReviseReplica) +} + +func (r *replicaRevisionRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { + if c.Spec.Customizations.ReplicaRevision != nil { + return c.Spec.Customizations.ReplicaRevision.LuaScript + } + return "" +} + +func (r *replicaRevisionRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) { + if script == "" { + c.Spec.Customizations.ReplicaRevision = nil + return + } + + if c.Spec.Customizations.ReplicaRevision == nil { + c.Spec.Customizations.ReplicaRevision = &configv1alpha1.ReplicaRevision{} + } + c.Spec.Customizations.ReplicaRevision.LuaScript = script +} + +func (r *replicaRevisionRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { + obj, err := args.getObjectOrError() + if err != nil { + return newRuleResultWithError(err) + } + revised, enabled, err := interpreter.ReviseReplica(obj, args.Replica) + if err != nil { + return newRuleResultWithError(err) + } + if !enabled { + return newRuleResultWithError(fmt.Errorf("rule is not enabled")) + } + return newRuleResult().add("revised", revised) +} + +type statusReflectionRule struct { +} + +func (s *statusReflectionRule) Name() string { + return string(configv1alpha1.InterpreterOperationInterpretStatus) +} + +func (s *statusReflectionRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { + if c.Spec.Customizations.StatusReflection != nil { + return c.Spec.Customizations.StatusReflection.LuaScript + } + return "" +} + +func (s *statusReflectionRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) { + if script == "" { + c.Spec.Customizations.StatusReflection = nil + return + } + + if c.Spec.Customizations.StatusReflection == nil { + c.Spec.Customizations.StatusReflection = &configv1alpha1.StatusReflection{} + } + c.Spec.Customizations.StatusReflection.LuaScript = script +} + +func (s *statusReflectionRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { + obj, err := args.getObjectOrError() + if err != nil { + return newRuleResultWithError(err) + } + status, enabled, err := interpreter.ReflectStatus(obj) + if err != nil { + return newRuleResultWithError(err) + } + if !enabled { + return newRuleResultWithError(fmt.Errorf("rule is not enabled")) + } + return newRuleResult().add("status", status) +} + +type statusAggregationRule struct { +} + +func (s *statusAggregationRule) Name() string { + return string(configv1alpha1.InterpreterOperationAggregateStatus) +} + +func (s *statusAggregationRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { + if c.Spec.Customizations.StatusAggregation != nil { + return c.Spec.Customizations.StatusAggregation.LuaScript + } + return "" +} + +func (s *statusAggregationRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) { + if script == "" { + c.Spec.Customizations.StatusAggregation = nil + return + } + + if c.Spec.Customizations.StatusAggregation == nil { + c.Spec.Customizations.StatusAggregation = &configv1alpha1.StatusAggregation{} + } + c.Spec.Customizations.StatusAggregation.LuaScript = script +} + +func (s *statusAggregationRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { + obj, err := args.getObjectOrError() + if err != nil { + return newRuleResultWithError(err) + } + aggregateStatus, enabled, err := interpreter.AggregateStatus(obj, args.Status) + if err != nil { + return newRuleResultWithError(err) + } + if !enabled { + return newRuleResultWithError(fmt.Errorf("rule is not enabled")) + } + return newRuleResult().add("aggregateStatus", aggregateStatus) +} + +type healthInterpretationRule struct { +} + +func (h *healthInterpretationRule) Name() string { + return string(configv1alpha1.InterpreterOperationInterpretHealth) +} + +func (h *healthInterpretationRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { + if c.Spec.Customizations.HealthInterpretation != nil { + return c.Spec.Customizations.Retention.LuaScript + } + return "" +} + +func (h *healthInterpretationRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) { + if script == "" { + c.Spec.Customizations.HealthInterpretation = nil + return + } + + if c.Spec.Customizations.HealthInterpretation == nil { + c.Spec.Customizations.HealthInterpretation = &configv1alpha1.HealthInterpretation{} + } + c.Spec.Customizations.HealthInterpretation.LuaScript = script +} + +func (h *healthInterpretationRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { + obj, err := args.getObjectOrError() + if err != nil { + return newRuleResultWithError(err) + } + healthy, enabled, err := interpreter.InterpretHealth(obj) + if err != nil { + return newRuleResultWithError(err) + } + if !enabled { + return newRuleResultWithError(fmt.Errorf("rule is not enabled")) + } + return newRuleResult().add("healthy", healthy) +} + +type dependencyInterpretationRule struct { +} + +func (d *dependencyInterpretationRule) Name() string { + return string(configv1alpha1.InterpreterOperationInterpretDependency) +} + +func (d *dependencyInterpretationRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { + if c.Spec.Customizations.DependencyInterpretation != nil { + return c.Spec.Customizations.Retention.LuaScript + } + return "" +} + +func (d *dependencyInterpretationRule) SetScript(c *configv1alpha1.ResourceInterpreterCustomization, script string) { + if script == "" { + c.Spec.Customizations.DependencyInterpretation = nil + return + } + + if c.Spec.Customizations.DependencyInterpretation == nil { + c.Spec.Customizations.DependencyInterpretation = &configv1alpha1.DependencyInterpretation{} + } + c.Spec.Customizations.DependencyInterpretation.LuaScript = script +} + +func (d *dependencyInterpretationRule) Run(interpreter *configurableinterpreter.ConfigurableInterpreter, args ruleArgs) *ruleResult { + obj, err := args.getObjectOrError() + if err != nil { + return newRuleResultWithError(err) + } + dependencies, enabled, err := interpreter.GetDependencies(obj) + if err != nil { + return newRuleResultWithError(err) + } + if !enabled { + return newRuleResultWithError(fmt.Errorf("rule is not enabled")) + } + return newRuleResult().add("dependencies", dependencies) +} + +// Rule known how to get and set script for interpretation rule, and can execute the rule with given args. +type Rule interface { + // Name returns the name of the rule. + Name() string + // GetScript returns the script for the rule from customization. If not enabled, return empty + GetScript(*configv1alpha1.ResourceInterpreterCustomization) string + // 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 +} + +// Rules is a series of rules. +type Rules []Rule + +// Names returns the names of containing rules. +func (r Rules) Names() []string { + names := make([]string, len(r)) + for i, rr := range r { + names[i] = rr.Name() + } + return names +} + +// Get returns the rule with the name. If not found, return nil. +func (r Rules) Get(name string) Rule { + for _, rr := range r { + if rr.Name() == name { + return rr + } + } + return nil +} + +type ruleArgs struct { + Desired *unstructured.Unstructured + Observed *unstructured.Unstructured + Status []workv1alpha2.AggregatedStatusItem + Replica int64 +} + +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) { + 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) { + if r.Desired == nil && r.Observed == nil { + return nil, fmt.Errorf("desired, desired-file, observed, observed-file options are not set") + } + if r.Desired != nil && r.Observed != nil { + return nil, fmt.Errorf("you can not specify multiple object by desired, desired-file, observed, observed-file options") + } + if r.Desired != nil { + return r.Desired, nil + } + return r.Observed, nil +} + +type nameValue struct { + Name string + Value interface{} +} + +type ruleResult struct { + Results []nameValue + Err error +} + +func newRuleResult() *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}) + return r +} diff --git a/pkg/karmadactl/karmadactl.go b/pkg/karmadactl/karmadactl.go index 55cedc4d52d5..1c5685db41e2 100644 --- a/pkg/karmadactl/karmadactl.go +++ b/pkg/karmadactl/karmadactl.go @@ -20,6 +20,7 @@ import ( "github.com/karmada-io/karmada/pkg/karmadactl/describe" "github.com/karmada-io/karmada/pkg/karmadactl/exec" "github.com/karmada-io/karmada/pkg/karmadactl/get" + "github.com/karmada-io/karmada/pkg/karmadactl/interpret" "github.com/karmada-io/karmada/pkg/karmadactl/join" "github.com/karmada-io/karmada/pkg/karmadactl/logs" "github.com/karmada-io/karmada/pkg/karmadactl/options" @@ -97,6 +98,7 @@ func NewKarmadaCtlCommand(cmdUse, parentCommand string) *cobra.Command { logs.NewCmdLogs(f, parentCommand, ioStreams), exec.NewCmdExec(f, parentCommand, ioStreams), describe.NewCmdDescribe(f, parentCommand, ioStreams), + interpret.NewCmdInterpret(f, parentCommand, ioStreams), }, }, { diff --git a/pkg/resourceinterpreter/configurableinterpreter/luavm/lua.go b/pkg/resourceinterpreter/configurableinterpreter/luavm/lua.go index 1a06b56340a4..bd6b36431f90 100644 --- a/pkg/resourceinterpreter/configurableinterpreter/luavm/lua.go +++ b/pkg/resourceinterpreter/configurableinterpreter/luavm/lua.go @@ -404,6 +404,25 @@ func (vm VM) GetDependencies(object *unstructured.Unstructured, script string) ( return } +// NewWithContext creates a lua VM with the given context. +func NewWithContext(ctx context.Context) (*lua.LState, error) { + vm := VM{} + l := lua.NewState(lua.Options{ + SkipOpenLibs: !vm.UseOpenLibs, + }) + // Opens table library to allow access to functions to manipulate tables + err := vm.setLib(l) + if err != nil { + return nil, err + } + // preload our 'safe' version of the OS library. Allows the 'local os = require("os")' to work + l.PreloadModule(lua.OsLibName, lifted.SafeOsLoader) + if ctx != nil { + l.SetContext(ctx) + } + return l, nil +} + // nolint:gocyclo func decodeValue(L *lua.LState, value interface{}) (lua.LValue, error) { // We handle simple type without json for better performance.