Merge pull request #2831 from ikaven1024/interpret-ctl-edit
add edit mode for karmadactl interpreter command
This commit is contained in:
commit
07bb65bfe7
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue