add karmadactl interpret subcommand

Signed-off-by: yingjinhui <yingjinhui@didiglobal.com>
This commit is contained in:
yingjinhui 2022-11-08 17:42:11 +08:00
parent 594ad9f44b
commit 81b2596ec1
6 changed files with 684 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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