Merge pull request #2831 from ikaven1024/interpret-ctl-edit

add edit mode for karmadactl interpreter command
This commit is contained in:
karmada-bot 2023-01-10 11:15:03 +08:00 committed by GitHub
commit 07bb65bfe7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 623 additions and 4 deletions

View File

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

View File

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

View File

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