diff --git a/pkg/karmadactl/interpret/edit.go b/pkg/karmadactl/interpret/edit.go new file mode 100644 index 000000000000..53f3484f8239 --- /dev/null +++ b/pkg/karmadactl/interpret/edit.go @@ -0,0 +1,508 @@ +package interpret + +import ( + "bufio" + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "strings" + + jsonpatch "github.com/evanphx/json-patch/v5" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/mergepatch" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/klog/v2" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/cmd/util/editor" + "k8s.io/kubectl/pkg/cmd/util/editor/crlf" + + configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" + "github.com/karmada-io/karmada/pkg/util/interpreter" +) + +func (o *Options) completeEdit() error { + if !o.Edit { + return nil + } + + o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { + o.PrintFlags.NamePrintFlags.Operation = operation + return o.PrintFlags.ToPrinter() + } + return nil +} + +// this logic is modified from: https://github.com/kubernetes/kubernetes/blob/70617042976dc168208a41b8a10caa61f9748617/staging/src/k8s.io/kubectl/pkg/cmd/util/editor/editoptions.go#L246-L479 +// nolint:gocyclo +func (o *Options) runEdit() error { + infos, err := o.CustomizationResult.Infos() + if err != nil { + return err + } + + var info *resource.Info + switch len(infos) { + case 0: + if len(o.Filenames) != 1 { + return fmt.Errorf("no customizations found. If you want to create a new one, please set only one file with '-f'") + } + info = &resource.Info{ + Source: o.Filenames[0], + Object: &configv1alpha1.ResourceInterpreterCustomization{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "config.karmada.io/v1alpha1", + Kind: "ResourceInterpreterCustomization", + }, + }, + } + case 1: + info = infos[0] + default: + return fmt.Errorf("only one customization can be edited") + } + + originalCustomization, err := asResourceInterpreterCustomization(info.Object) + if err != nil { + return err + } + editedCustomization := originalCustomization.DeepCopy() + editedCustomization.Spec = configv1alpha1.ResourceInterpreterCustomizationSpec{} + + var ( + edit = editor.NewDefaultEditor(editorEnvs()) + results = editResults{} + edited = []byte{} + file string + containsError = false + ) + + // loop until we succeed or cancel editing + for { + // generate the file to edit + buf := &bytes.Buffer{} + var w io.Writer = buf + if o.WindowsLineEndings { + w = crlf.NewCRLFWriter(w) + } + + err = results.header.writeTo(w) + if err != nil { + return err + } + + if !containsError { + printCustomization(w, originalCustomization, o.Rules, o.ShowDoc) + } else { + // In case of an error, preserve the edited file. + // Remove the comments (header) from it since we already + // have included the latest header in the buffer above. + buf.Write(stripComments(edited)) + } + + // launch the editor + editedDiff := edited + edited, file, err = edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), ".lua", buf) + if err != nil { + return preservedFile(err, results.file, o.ErrOut) + } + + // If we're retrying the loop because of an error, and no change was made in the file, short-circuit + if containsError && bytes.Equal(stripComments(editedDiff), stripComments(edited)) { + return preservedFile(fmt.Errorf("%s", "Edit cancelled, no valid changes were saved."), file, o.ErrOut) + } + // cleanup any file from the previous pass + if len(results.file) > 0 { + os.Remove(results.file) + } + klog.V(4).Infof("User edited:\n%s", string(edited)) + + // build new customization from edited file + err = parseEditedIntoCustomization(edited, editedCustomization, o.Rules) + if err != nil { + results = editResults{ + file: file, + } + containsError = true + fmt.Fprintln(o.ErrOut, results.addError(apierrors.NewInvalid(corev1.SchemeGroupVersion.WithKind("").GroupKind(), + "", field.ErrorList{field.Invalid(nil, "The edited file failed validation", fmt.Sprintf("%v", err))}), info)) + continue + } + + // Compare content + if isEqualsCustomization(originalCustomization, editedCustomization) { + os.Remove(file) + fmt.Fprintln(o.ErrOut, "Edit cancelled, no changes made.") + return nil + } + + results = editResults{ + file: file, + } + + // TODO: validate edited customization + + // not a syntax error as it turns out... + containsError = false + + // TODO: add last-applied-configuration annotation + + switch { + case info.Source != "": + err = o.saveToPath(info, editedCustomization, &results) + default: + err = o.saveToServer(info, editedCustomization, &results) + } + if err != nil { + return preservedFile(err, results.file, o.ErrOut) + } + + // Handle all possible errors + // + // 1. retryable: propose kubectl replace -f + // 2. notfound: indicate the location of the saved configuration of the deleted resource + // 3. invalid: retry those on the spot by looping ie. reloading the editor + if results.retryable > 0 { + fmt.Fprintf(o.ErrOut, "You can run `%s replace -f %s` to try this update again.\n", filepath.Base(os.Args[0]), file) + return cmdutil.ErrExit + } + if results.notfound > 0 { + fmt.Fprintf(o.ErrOut, "The edits you made on deleted resources have been saved to %q\n", file) + return cmdutil.ErrExit + } + + if len(results.edit) == 0 { + if results.notfound == 0 { + os.Remove(file) + } else { + fmt.Fprintf(o.Out, "The edits you made on deleted resources have been saved to %q\n", file) + } + return nil + } + + if len(results.header.reasons) > 0 { + containsError = true + } + } +} + +func (o *Options) saveToPath(originalInfo *resource.Info, editedObj runtime.Object, results *editResults) error { + var w io.Writer + var writer printers.ResourcePrinter = &printers.YAMLPrinter{} + source := originalInfo.Source + switch { + case source == "": + return fmt.Errorf("resource %s/%s is not from file", originalInfo.Namespace, originalInfo.Name) + case source == "STDIN" || strings.HasPrefix(source, "http"): + w = os.Stdout + default: + f, err := os.OpenFile(originalInfo.Source, os.O_RDWR|os.O_TRUNC, 0) + if err != nil { + return err + } + defer f.Close() + w = f + + _, _, isJSON := yaml.GuessJSONStream(f, 4096) + if isJSON { + writer = &printers.JSONPrinter{} + } + } + + err := writer.PrintObj(editedObj, w) + if err != nil { + fmt.Fprint(o.ErrOut, results.addError(err, originalInfo)) + } + + printer, err := o.ToPrinter("edited") + if err != nil { + return err + } + return printer.PrintObj(originalInfo.Object, o.Out) +} + +func (o *Options) saveToServer(originalInfo *resource.Info, editedObj runtime.Object, results *editResults) error { + originalJS, err := json.Marshal(originalInfo.Object) + if err != nil { + return err + } + + editedJS, err := json.Marshal(editedObj) + if err != nil { + return err + } + + preconditions := []mergepatch.PreconditionFunc{ + mergepatch.RequireKeyUnchanged("apiVersion"), + mergepatch.RequireKeyUnchanged("kind"), + mergepatch.RequireMetadataKeyUnchanged("name"), + mergepatch.RequireKeyUnchanged("managedFields"), + } + + patchType := types.MergePatchType + patch, err := jsonpatch.CreateMergePatch(originalJS, editedJS) + if err != nil { + klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) + return err + } + var patchMap map[string]interface{} + err = json.Unmarshal(patch, &patchMap) + if err != nil { + klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) + return err + } + for _, precondition := range preconditions { + if !precondition(patchMap) { + klog.V(4).Infof("Unable to calculate diff, no merge is possible: %v", err) + return fmt.Errorf("%s", "At least one of apiVersion, kind and name was changed") + } + } + + if o.OutputPatch { + fmt.Fprintf(o.Out, "Patch: %s\n", string(patch)) + } + + patched, err := resource.NewHelper(originalInfo.Client, originalInfo.Mapping). + WithFieldManager(o.FieldManager). + WithFieldValidation(o.ValidationDirective). + WithSubresource(o.Subresource). + Patch(originalInfo.Namespace, originalInfo.Name, patchType, patch, nil) + if err != nil { + fmt.Fprintln(o.ErrOut, results.addError(err, originalInfo)) + return nil + } + err = originalInfo.Refresh(patched, true) + if err != nil { + return err + } + printer, err := o.ToPrinter("edited") + if err != nil { + return err + } + return printer.PrintObj(originalInfo.Object, o.Out) +} + +func isEqualsCustomization(a, b *configv1alpha1.ResourceInterpreterCustomization) bool { + return a.Namespace == b.Namespace && a.Name == b.Name && reflect.DeepEqual(a.Spec, b.Spec) +} + +const ( + luaCommentPrefix = "--" + luaAnnotationPrefix = "---@" + luaAnnotationName = luaAnnotationPrefix + "name:" + luaAnnotationAPIVersion = luaAnnotationPrefix + "apiVersion:" + luaAnnotationKind = luaAnnotationPrefix + "kind:" + luaAnnotationRule = luaAnnotationPrefix + "rule:" +) + +func printCustomization(w io.Writer, c *configv1alpha1.ResourceInterpreterCustomization, rules interpreter.Rules, showDoc bool) { + fmt.Fprintf(w, "%s %s\n", luaAnnotationName, c.Name) + fmt.Fprintf(w, "%s %s\n", luaAnnotationAPIVersion, c.Spec.Target.APIVersion) + fmt.Fprintf(w, "%s %s\n", luaAnnotationKind, c.Spec.Target.Kind) + for _, r := range rules { + fmt.Fprintf(w, "%s %s\n", luaAnnotationRule, r.Name()) + if showDoc { + if doc := r.Document(); doc != "" { + fmt.Fprintf(w, "%s %s\n", luaCommentPrefix, commentOnLineBreak(doc)) + } + } + if script := r.GetScript(c); script != "" { + fmt.Fprintf(w, "%s", script) + } + } +} + +// The file is like: +// -- Please edit the object below. Lines beginning with a '--' will be ignored, +// -- and an empty file will abort the edit. If an error occurs while saving this file will be +// -- reopened with the relevant failures. +// -- +// ---@name: foo +// ---@apiVersion: apps/v1 +// ---@kind: Deployment +// ---@rule: Retain +// -- This rule is used to retain runtime values to the desired specification. +// -- The script should implement a function as follows: +// -- function Retain(desiredObj, observedObj) +// -- desiredObj.spec.fieldFoo = observedObj.spec.fieldFoo +// -- return desiredObj +// -- end +// function Retain(desiredObj, runtimeObj) +// +// desiredObj.spec.fieldFoo = runtimeObj.spec.fieldFoo +// return desiredObj +// +// end +func parseEditedIntoCustomization(file []byte, into *configv1alpha1.ResourceInterpreterCustomization, rules interpreter.Rules) error { + var currRule interpreter.Rule + var script string + scanner := bufio.NewScanner(bytes.NewBuffer(file)) + for scanner.Scan() { + line := scanner.Text() + trimline := strings.TrimSpace(line) + + switch { + case strings.HasPrefix(trimline, luaAnnotationName): + into.Name = strings.TrimSpace(strings.TrimPrefix(trimline, luaAnnotationName)) + case strings.HasPrefix(trimline, luaAnnotationAPIVersion): + into.Spec.Target.APIVersion = strings.TrimSpace(strings.TrimPrefix(trimline, luaAnnotationAPIVersion)) + case strings.HasPrefix(trimline, luaAnnotationKind): + into.Spec.Target.Kind = strings.TrimSpace(strings.TrimPrefix(trimline, luaAnnotationKind)) + case strings.HasPrefix(trimline, luaAnnotationRule): + name := strings.TrimSpace(strings.TrimPrefix(trimline, luaAnnotationRule)) + r := rules.Get(name) + if r != nil { + if currRule != nil { + currRule.SetScript(into, script) + } + currRule = r + script = "" + } + case strings.HasPrefix(trimline, luaCommentPrefix): + // comments are skipped + default: + if currRule == nil { + return fmt.Errorf("unexpected line %q", line) + } + script += string(line) + "\n" + } + } + + if currRule != nil { + currRule.SetScript(into, script) + } + return nil +} + +func stripComments(file []byte) []byte { + stripped := make([]byte, 0, len(file)) + + lines := bytes.Split(file, []byte("\n")) + for _, line := range lines { + trimline := bytes.TrimSpace(line) + if bytes.HasPrefix(trimline, []byte(luaCommentPrefix)) && !bytes.HasPrefix(trimline, []byte(luaAnnotationPrefix)) { + continue + } + if len(stripped) != 0 { + stripped = append(stripped, '\n') + } + stripped = append(stripped, line...) + } + return stripped +} + +// editReason preserves a message about the reason this file must be edited again +type editReason struct { + head string + other []string +} + +// editHeader includes a list of reasons the edit must be retried +type editHeader struct { + reasons []editReason +} + +// writeTo outputs the current header information into a stream +func (h *editHeader) writeTo(w io.Writer) error { + writeComment(w, `Please edit the object below. Lines beginning with a '--' will be ignored, +and an empty file will abort the edit. If an error occurs while saving this file will be +reopened with the relevant failures. + +`) + + for _, r := range h.reasons { + if len(r.other) > 0 { + writeComment(w, r.head+":\n") + } else { + writeComment(w, r.head+"\n") + } + for _, o := range r.other { + writeComment(w, o+"\n") + } + fmt.Fprintln(w, luaCommentPrefix) + } + return nil +} + +// editResults capture the result of an update +type editResults struct { + header editHeader + retryable int + notfound int + edit []*resource.Info + file string +} + +func (r *editResults) addError(err error, info *resource.Info) string { + switch { + case apierrors.IsInvalid(err): + r.edit = append(r.edit, info) + reason := editReason{ + head: fmt.Sprintf("%s %q was not valid", customizationResourceName, info.Name), + } + if err, ok := err.(apierrors.APIStatus); ok { + if details := err.Status().Details; details != nil { + for _, cause := range details.Causes { + reason.other = append(reason.other, fmt.Sprintf("%s: %s", cause.Field, cause.Message)) + } + } + } + r.header.reasons = append(r.header.reasons, reason) + return fmt.Sprintf("error: %s %q is invalid", customizationResourceName, info.Name) + case apierrors.IsNotFound(err): + r.notfound++ + return fmt.Sprintf("error: %s %q could not be found on the server", customizationResourceName, info.Name) + default: + r.retryable++ + return fmt.Sprintf("error: %s %q could not be patched: %v", customizationResourceName, info.Name, err) + } +} + +func writeComment(w io.Writer, comment string) { + fmt.Fprintf(w, "%s %s", luaCommentPrefix, commentOnLineBreak(comment)) +} + +// commentOnLineBreak returns a string built from the provided string by inserting any necessary '-- ' +// characters after '\n' characters, indicating a comment. +func commentOnLineBreak(s string) string { + r := "" + for i, ch := range s { + j := i + 1 + if j < len(s) && ch == '\n' && s[j] != '-' { + r += "\n-- " + } else { + r += string(ch) + } + } + return r +} + +// editorEnvs returns an ordered list of env vars to check for editor preferences. +func editorEnvs() []string { + return []string{ + "KUBE_EDITOR", + "EDITOR", + } +} + +// preservedFile writes out a message about the provided file if it exists to the +// provided output stream when an error happens. Used to notify the user where +// their updates were preserved. +func preservedFile(err error, path string, out io.Writer) error { + if len(path) > 0 { + if _, err := os.Stat(path); !os.IsNotExist(err) { + fmt.Fprintf(out, "A copy of your changes has been stored to %q\n", path) + } + } + return err +} diff --git a/pkg/karmadactl/interpret/interpret.go b/pkg/karmadactl/interpret/interpret.go index 7a17a07b216a..e84e24ede877 100644 --- a/pkg/karmadactl/interpret/interpret.go +++ b/pkg/karmadactl/interpret/interpret.go @@ -11,6 +11,7 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/cmd/util/editor" "k8s.io/kubectl/pkg/util/templates" configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" @@ -25,13 +26,14 @@ import ( var ( interpretLong = templates.LongDesc(` - Validate and test interpreter customization before applying it to the control plane. + Validate, test and edit 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. + 3. Edit customization. Similar to the kubectl edit. `) interpretExample = templates.Examples(` @@ -62,6 +64,8 @@ var ( # Fetch observed object from url, and status items from stdin (specified with -) %[1]s interpret -f customization.yml --operation aggregateStatus --observed-file https://example.com/observed.yml --status-file - + # Edit customization + %[1]s interpret -f customization.yml --edit `) ) @@ -71,13 +75,17 @@ const ( // NewCmdInterpret new interpret command. func NewCmdInterpret(f util.Factory, parentCommand string, streams genericclioptions.IOStreams) *cobra.Command { + editorFlags := editor.NewEditOptions(editor.NormalEditMode, streams) + editorFlags.PrintFlags = editorFlags.PrintFlags.WithTypeSetter(gclient.NewSchema()) + o := &Options{ - IOStreams: streams, - Rules: interpreter.AllResourceInterpreterCustomizationRules, + EditOptions: editorFlags, + IOStreams: streams, + Rules: interpreter.AllResourceInterpreterCustomizationRules, } cmd := &cobra.Command{ Use: "interpret (-f FILENAME) (--operation OPERATION) [--ARGS VALUE]... ", - Short: "Validate and test interpreter customization before applying it to the control plane", + Short: "Validate, test and edit interpreter customization before applying it to the control plane", Long: interpretLong, SilenceUsage: true, DisableFlagsInUseLine: true, @@ -94,8 +102,12 @@ func NewCmdInterpret(f util.Factory, parentCommand string, streams genericcliopt flags := cmd.Flags() options.AddKubeConfigFlags(flags) + o.EditOptions.RecordFlags.AddFlags(cmd) + o.EditOptions.PrintFlags.AddFlags(cmd) 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.BoolVar(&o.Edit, "edit", false, "Edit customizations") + flags.BoolVar(&o.ShowDoc, "show-doc", false, "Show document of rules when editing") 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.StringVar(&o.StatusFile, "status-file", o.StatusFile, "Filename, directory, or URL to files identifying the resource to use as statusItems argument in rule script.") @@ -109,9 +121,12 @@ func NewCmdInterpret(f util.Factory, parentCommand string, streams genericcliopt // Options contains the input to the interpret command. type Options struct { resource.FilenameOptions + *editor.EditOptions Operation string Check bool + Edit bool + ShowDoc bool // args DesiredFile string @@ -131,6 +146,10 @@ type Options struct { // Complete ensures that options are valid and marshals them if necessary func (o *Options) Complete(f util.Factory, cmd *cobra.Command, args []string) error { + if o.Check && o.Edit { + return fmt.Errorf("you can't set both --check and --edit options") + } + scheme := gclient.NewSchema() o.CustomizationResult = f.NewBuilder(). WithScheme(scheme, scheme.PrioritizedVersionsAllGroups()...). @@ -143,6 +162,7 @@ func (o *Options) Complete(f util.Factory, cmd *cobra.Command, args []string) er var errs []error errs = append(errs, o.CustomizationResult.Err()) errs = append(errs, o.completeExecute(f)...) + errs = append(errs, o.completeEdit()) return errors.NewAggregate(errs) } @@ -154,6 +174,12 @@ func (o *Options) Validate() error { return fmt.Errorf("operation %s is not supported. Use one of: %s", o.Operation, strings.Join(o.Rules.Names(), ", ")) } } + if o.Edit { + err := o.EditOptions.Validate() + if err != nil { + return err + } + } return nil } @@ -162,6 +188,8 @@ func (o *Options) Run() error { switch { case o.Check: return o.runCheck() + case o.Edit: + return o.runEdit() default: return o.runExecute() } diff --git a/pkg/util/interpreter/rule.go b/pkg/util/interpreter/rule.go index f151c4408195..b8eeae17cfbc 100644 --- a/pkg/util/interpreter/rule.go +++ b/pkg/util/interpreter/rule.go @@ -28,6 +28,15 @@ func (r *retentionRule) Name() string { return string(configv1alpha1.InterpreterOperationRetain) } +func (r *retentionRule) Document() string { + return `This rule is used to retain runtime values to the desired specification. +The script should implement a function as follows: +function Retain(desiredObj, observedObj) + desiredObj.spec.fieldFoo = observedObj.spec.fieldFoo + return desiredObj +end` +} + func (r *retentionRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { if c.Spec.Customizations.Retention != nil { return c.Spec.Customizations.Retention.LuaScript @@ -73,6 +82,19 @@ func (r *replicaResourceRule) Name() string { return string(configv1alpha1.InterpreterOperationInterpretReplica) } +func (r *replicaResourceRule) Document() string { + return `This rule is used to discover the resource's replica as well as resource requirements. +The script should implement a function as follows: +function GetReplicas(desiredObj) + replica = desiredObj.spec.replicas + nodeClaim = {} + nodeClaim.hardNodeAffinity = {} + nodeClaim.nodeSelector = {} + nodeClaim.tolerations = {} + return replica, nodeClaim +end` +} + func (r *replicaResourceRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { if c.Spec.Customizations.ReplicaResource != nil { return c.Spec.Customizations.ReplicaResource.LuaScript @@ -114,6 +136,15 @@ func (r *replicaRevisionRule) Name() string { return string(configv1alpha1.InterpreterOperationReviseReplica) } +func (r *replicaRevisionRule) Document() string { + return `This rule is used to revise replicas in the desired specification. +The script should implement a function as follows: +function ReviseReplica(desiredObj, desiredReplica) + desiredObj.spec.replicas = desiredReplica + return desiredObj +end` +} + func (r *replicaRevisionRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { if c.Spec.Customizations.ReplicaRevision != nil { return c.Spec.Customizations.ReplicaRevision.LuaScript @@ -155,6 +186,16 @@ func (s *statusReflectionRule) Name() string { return string(configv1alpha1.InterpreterOperationInterpretStatus) } +func (s *statusReflectionRule) Document() string { + return `This rule is used to get the status from the observed specification. +The script should implement a function as follows: +function ReflectStatus(observedObj) + status = {} + status.readyReplicas = observedObj.status.observedObj + return status +end` +} + func (s *statusReflectionRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { if c.Spec.Customizations.StatusReflection != nil { return c.Spec.Customizations.StatusReflection.LuaScript @@ -196,6 +237,17 @@ func (s *statusAggregationRule) Name() string { return string(configv1alpha1.InterpreterOperationAggregateStatus) } +func (s *statusAggregationRule) Document() string { + return `This rule is used to aggregate decentralized statuses to the desired specification. +The script should implement a function as follows: +function AggregateStatus(desiredObj, statusItems) + for i = 1, #items do + desiredObj.status.readyReplicas = desiredObj.status.readyReplicas + items[i].readyReplicas + end + return desiredObj +end` +} + func (s *statusAggregationRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { if c.Spec.Customizations.StatusAggregation != nil { return c.Spec.Customizations.StatusAggregation.LuaScript @@ -242,6 +294,17 @@ func (h *healthInterpretationRule) Name() string { return string(configv1alpha1.InterpreterOperationInterpretHealth) } +func (h *healthInterpretationRule) Document() string { + return `This rule is used to assess the health state of a specific resource. +The script should implement a function as follows: +luaScript: > +function InterpretHealth(observedObj) + if observedObj.status.readyReplicas == observedObj.spec.replicas then + return true + end +end` +} + func (h *healthInterpretationRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { if c.Spec.Customizations.HealthInterpretation != nil { return c.Spec.Customizations.HealthInterpretation.LuaScript @@ -283,6 +346,24 @@ func (d *dependencyInterpretationRule) Name() string { return string(configv1alpha1.InterpreterOperationInterpretDependency) } +func (d *dependencyInterpretationRule) Document() string { + return ` This rule is used to interpret the dependencies of a specific resource. +The script should implement a function as follows: +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` +} + func (d *dependencyInterpretationRule) GetScript(c *configv1alpha1.ResourceInterpreterCustomization) string { if c.Spec.Customizations.DependencyInterpretation != nil { return c.Spec.Customizations.DependencyInterpretation.LuaScript @@ -321,6 +402,8 @@ func (d *dependencyInterpretationRule) Run(interpreter *configurableinterpreter. type Rule interface { // Name returns the name of the rule. Name() string + // Document explains detail of rule. + Document() 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.