Skip to content

Commit

Permalink
add execute mode for karmadactl interpret subcommand
Browse files Browse the repository at this point in the history
Signed-off-by: yingjinhui <yingjinhui@didiglobal.com>
  • Loading branch information
ikaven1024 committed Nov 24, 2022
1 parent 63a67d7 commit f70eafd
Show file tree
Hide file tree
Showing 11 changed files with 813 additions and 18 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ require (
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64
go.uber.org/atomic v1.7.0
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/text v0.3.7
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
golang.org/x/tools v0.1.12
gomodules.xyz/jsonpatch/v2 v2.2.0
google.golang.org/grpc v1.47.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.25.4
k8s.io/apiextensions-apiserver v0.25.4
k8s.io/apimachinery v0.25.4
Expand Down Expand Up @@ -159,7 +161,6 @@ require (
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21 // indirect
google.golang.org/protobuf v1.28.0 // indirect
Expand All @@ -168,7 +169,6 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/square/go-jose.v2 v2.2.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/gengo v0.0.0-20211129171323-c02415ce4185 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.33 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
Expand Down
147 changes: 144 additions & 3 deletions pkg/karmadactl/interpret/execute.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,157 @@
package interpret

import (
"encoding/json"
"fmt"
"io"
"strings"

"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"k8s.io/cli-runtime/pkg/resource"
"k8s.io/kubectl/pkg/cmd/util"

workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
"github.com/karmada-io/karmada/pkg/karmadactl/util/genericresource"
"github.com/karmada-io/karmada/pkg/resourceinterpreter/configurableinterpreter"
)

func (o *Options) completeExecute(_ util.Factory, _ *cobra.Command, _ []string) []error {
return nil
func (o *Options) completeExecute(f util.Factory, _ *cobra.Command, _ []string) []error {
var errs []error
if o.DesiredFile != "" {
o.DesiredResult = f.NewBuilder().
Unstructured().
FilenameParam(false, &resource.FilenameOptions{Filenames: []string{o.DesiredFile}}).
RequireObject(true).
Local().
Do()
errs = append(errs, o.DesiredResult.Err())
}

if o.ObservedFile != "" {
o.ObservedResult = f.NewBuilder().
Unstructured().
FilenameParam(false, &resource.FilenameOptions{Filenames: []string{o.ObservedFile}}).
RequireObject(true).
Local().
Do()
errs = append(errs, o.ObservedResult.Err())
}

if len(o.StatusFile) > 0 {
o.StatusResult = genericresource.NewBuilder().
Constructor(func() interface{} { return &workv1alpha2.AggregatedStatusItem{} }).
Filename(true, o.StatusFile).
Do()
errs = append(errs, o.StatusResult.Err())
}
return errs
}

func (o *Options) runExecute() error {
return fmt.Errorf("not implement")
if o.Operation == "" {
return fmt.Errorf("operation is not set for executing")
}

customizations, err := o.getCustomizationObject()
if err != nil {
return fmt.Errorf("fail to get customization object: %v", err)
}

desired, err := getUnstructuredObjectFromResult(o.DesiredResult)
if err != nil {
return fmt.Errorf("fail to get desired object: %v", err)
}

observed, err := getUnstructuredObjectFromResult(o.ObservedResult)
if err != nil {
return fmt.Errorf("fail to get observed object: %v", err)
}

status, err := o.getAggregatedStatusItems()
if err != nil {
return fmt.Errorf("fail to get status items: %v", err)
}

args := ruleArgs{
Desired: desired,
Observed: observed,
Status: status,
Replica: int64(o.DesiredReplica),
}

interpreter := configurableinterpreter.NewConfigurableInterpreter(nil)
interpreter.LoadConfig(customizations)

r := o.Rules.GetByOperation(o.Operation)
if r == nil {
// Shall never occur, because we validate it before.
return fmt.Errorf("operation %s is not supported. Use one of: %s", o.Operation, strings.Join(o.Rules.Names(), ", "))
}
result := r.Run(interpreter, args)
printExecuteResult(o.Out, o.ErrOut, r.Name(), result)
return nil
}

func printExecuteResult(w, errOut io.Writer, name string, result *ruleResult) {
if result.Err != nil {
fmt.Fprintf(errOut, "Execute %s error: %v\n", name, result.Err)
return
}

for i, res := range result.Results {
func() {
fmt.Fprintln(w, "---")
fmt.Fprintf(w, "# [%v/%v] %s:\n", i+1, len(result.Results), res.Name)
if err := printObjectYaml(w, res.Value); err != nil {
fmt.Fprintf(errOut, "ERROR: %v\n", err)
}
}()
}
}

// MarshalJSON doesn't work for yaml encoder, so unstructured.Unstructured and runtime.RawExtension objects
// will be encoded into unexpected data.
// Example1:
//
// &unstructured.Unstructured{
// Object: map[string]interface{}{
// "foo": "bar"
// },
// }
//
// will be encoded into:
//
// Object:
// foo: bar
//
// Example2:
//
// &runtime.RawExtension{
// Raw: []byte("{}"),
// }
//
// will be encoded into:
//
// raw:
// - 123
// - 125
//
// Inspired from https://github.com/kubernetes/kubernetes/blob/8fb423bfabe0d53934cc94c154c7da2dc3ce1332/staging/src/k8s.io/kubectl/pkg/cmd/get/get.go#L781-L786
// we convert it to map[string]interface{} by json, then encode the converted object to yaml.
func printObjectYaml(w io.Writer, obj interface{}) error {
data, err := json.Marshal(obj)
if err != nil {
return err
}

var converted map[string]interface{}
err = json.Unmarshal(data, &converted)
if err != nil {
return err
}

encoder := yaml.NewEncoder(w)
defer encoder.Close()
return encoder.Encode(converted)
}
78 changes: 75 additions & 3 deletions pkg/karmadactl/interpret/interpret.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"

"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/errors"
"k8s.io/cli-runtime/pkg/genericclioptions"
Expand All @@ -13,9 +14,12 @@ import (
"k8s.io/kubectl/pkg/util/templates"

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/karmadactl/options"
"github.com/karmada-io/karmada/pkg/karmadactl/util"
"github.com/karmada-io/karmada/pkg/karmadactl/util/genericresource"
"github.com/karmada-io/karmada/pkg/util/gclient"
"github.com/karmada-io/karmada/pkg/util/helper"
)

var (
Expand All @@ -32,18 +36,27 @@ var (
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
%[1]s interpret -f customization.yml --operation aggregateStatus --observed-file observed.yml --status-file status.yml
# Fetch arguments from url and stdin
%[1]s interpret -f customization.yml --operation aggregateStatus --observed-file https://example.com/observed.yml --status-file -
`)
)
Expand Down Expand Up @@ -81,7 +94,7 @@ func NewCmdInterpret(f util.Factory, parentCommand string, streams genericcliopt
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.StringVar(&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.")
Expand All @@ -99,10 +112,13 @@ type Options struct {
// args
DesiredFile string
ObservedFile string
StatusFile []string
StatusFile string
DesiredReplica int32

CustomizationResult *resource.Result
DesiredResult *resource.Result
ObservedResult *resource.Result
StatusResult *genericresource.Result

Rules Rules

Expand All @@ -128,6 +144,12 @@ func (o *Options) Complete(f util.Factory, cmd *cobra.Command, args []string) er

// Validate checks the EditOptions to see if there is sufficient information to run the command.
func (o *Options) Validate() error {
if o.Operation != "" {
r := o.Rules.GetByOperation(o.Operation)
if r == nil {
return fmt.Errorf("operation %s is not supported. Use one of: %s", o.Operation, strings.Join(o.Rules.Names(), ", "))
}
}
return nil
}

Expand All @@ -141,6 +163,56 @@ func (o *Options) Run() error {
}
}

func (o *Options) getCustomizationObject() ([]*configv1alpha1.ResourceInterpreterCustomization, error) {
infos, err := o.CustomizationResult.Infos()
if err != nil {
return nil, err
}

customizations := make([]*configv1alpha1.ResourceInterpreterCustomization, len(infos))
for i, info := range infos {
c, err := asResourceInterpreterCustomization(info.Object)
if err != nil {
return nil, err
}
customizations[i] = c
}
return customizations, nil
}

func (o *Options) getAggregatedStatusItems() ([]workv1alpha2.AggregatedStatusItem, error) {
if o.StatusResult == nil {
return nil, nil
}

objs, err := o.StatusResult.Objects()
if err != nil {
return nil, err
}
items := make([]workv1alpha2.AggregatedStatusItem, len(objs))
for i, obj := range objs {
items[i] = *(obj.(*workv1alpha2.AggregatedStatusItem))
}
return items, nil
}

func getUnstructuredObjectFromResult(result *resource.Result) (*unstructured.Unstructured, error) {
if result == nil {
return nil, nil
}

infos, err := result.Infos()
if err != nil {
return nil, err
}

if len(infos) > 1 {
return nil, fmt.Errorf("get %v objects, expect one at most", len(infos))
}

return helper.ToUnstructured(infos[0].Object)
}

func asResourceInterpreterCustomization(o runtime.Object) (*configv1alpha1.ResourceInterpreterCustomization, error) {
c, ok := o.(*configv1alpha1.ResourceInterpreterCustomization)
if !ok {
Expand Down
29 changes: 25 additions & 4 deletions pkg/karmadactl/interpret/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package interpret

import (
"fmt"
"strings"

"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

Expand Down Expand Up @@ -218,14 +219,19 @@ func (s *statusAggregationRule) Run(interpreter *configurableinterpreter.Configu
if err != nil {
return newRuleResultWithError(err)
}
aggregateStatus, enabled, err := interpreter.AggregateStatus(obj, args.Status)

status := args.Status
if status == nil {
status = []workv1alpha2.AggregatedStatusItem{}
}
aggregateStatus, enabled, err := interpreter.AggregateStatus(obj, status)
if err != nil {
return newRuleResultWithError(err)
}
if !enabled {
return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
}
return newRuleResult().add("aggregateStatus", aggregateStatus)
return newRuleResult().add("aggregatedStatus", aggregateStatus)
}

type healthInterpretationRule struct {
Expand Down Expand Up @@ -334,6 +340,21 @@ func (r Rules) Names() []string {
return names
}

// GetByOperation returns the matched rule by operation name, ignoring case. Return nil if none is matched.
func (r Rules) GetByOperation(operation string) Rule {
if operation == "" {
return nil
}
operation = strings.ToLower(operation)
for _, rule := range r {
ruleName := strings.ToLower(rule.Name())
if ruleName == operation {
return rule
}
}
return nil
}

// Get returns the rule with the name. If not found, return nil.
func (r Rules) Get(name string) Rule {
for _, rr := range r {
Expand Down Expand Up @@ -367,10 +388,10 @@ func (r ruleArgs) getObservedObjectOrError() (*unstructured.Unstructured, error)

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")
return nil, fmt.Errorf("desired-file, 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")
return nil, fmt.Errorf("you can not specify both desired-file and observed-file options")
}
if r.Desired != nil {
return r.Desired, nil
Expand Down
Loading

0 comments on commit f70eafd

Please sign in to comment.