github.com/GoogleContainerTools/kpt@v1.0.0-beta.50.0.20240520170205-c25345ffcbee/commands/alpha/live/plan/command.go (about)

     1  // Copyright 2022 The kpt Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package plan
    16  
    17  import (
    18  	"bufio"
    19  	"context"
    20  	"fmt"
    21  	"os"
    22  	"strings"
    23  
    24  	"github.com/GoogleContainerTools/kpt/internal/util/argutil"
    25  	"github.com/GoogleContainerTools/kpt/pkg/live"
    26  	kptplanner "github.com/GoogleContainerTools/kpt/pkg/live/planner"
    27  	"github.com/spf13/cobra"
    28  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    29  	"k8s.io/cli-runtime/pkg/genericclioptions"
    30  	"k8s.io/kubectl/pkg/cmd/util"
    31  	"sigs.k8s.io/cli-utils/cmd/flagutils"
    32  	"sigs.k8s.io/cli-utils/pkg/common"
    33  	print "sigs.k8s.io/cli-utils/pkg/print/common"
    34  	"sigs.k8s.io/kustomize/kyaml/kio"
    35  	"sigs.k8s.io/kustomize/kyaml/yaml"
    36  )
    37  
    38  const (
    39  	TextOutput = "text"
    40  	KRMOutput  = "krm"
    41  
    42  	EntryPrefix   = "\t"
    43  	ContentPrefix = "\t\t"
    44  )
    45  
    46  func NewRunner(ctx context.Context, factory util.Factory, ioStreams genericclioptions.IOStreams) *Runner {
    47  	r := &Runner{
    48  		ctx:       ctx,
    49  		factory:   factory,
    50  		ioStreams: ioStreams,
    51  		serverSideOptions: common.ServerSideOptions{
    52  			ServerSideApply: true,
    53  		},
    54  	}
    55  	c := &cobra.Command{
    56  		Use:     "plan [PKG_PATH | -]",
    57  		PreRunE: r.PreRunE,
    58  		RunE:    r.RunE,
    59  	}
    60  	c.Flags().StringVar(&r.inventoryPolicyString, flagutils.InventoryPolicyFlag, flagutils.InventoryPolicyStrict,
    61  		"It determines the behavior when the resources don't belong to current inventory. Available options "+
    62  			fmt.Sprintf("%q and %q.", flagutils.InventoryPolicyStrict, flagutils.InventoryPolicyAdopt))
    63  	c.Flags().BoolVar(&r.serverSideOptions.ForceConflicts, "force-conflicts", false,
    64  		"If true, overwrite applied fields on server if field manager conflict.")
    65  	c.Flags().StringVar(&r.serverSideOptions.FieldManager, "field-manager", common.DefaultFieldManager,
    66  		"The client owner of the fields being applied on the server-side.")
    67  	c.Flags().StringVar(&r.output, "output", "text",
    68  		"The output format for the plan. Must be either 'text' or 'krm'. Default is 'text'")
    69  	r.Command = c
    70  
    71  	return r
    72  }
    73  
    74  func NewCommand(ctx context.Context, factory util.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
    75  	return NewRunner(ctx, factory, ioStreams).Command
    76  }
    77  
    78  type Runner struct {
    79  	ctx       context.Context
    80  	Command   *cobra.Command
    81  	factory   util.Factory
    82  	ioStreams genericclioptions.IOStreams
    83  
    84  	inventoryPolicyString string
    85  	serverSideOptions     common.ServerSideOptions
    86  	output                string
    87  }
    88  
    89  func (r *Runner) PreRunE(_ *cobra.Command, _ []string) error {
    90  	return r.validateOutputFormat()
    91  }
    92  
    93  func (r *Runner) validateOutputFormat() error {
    94  	if !(r.output == "text" || r.output == "krm") {
    95  		return fmt.Errorf("unknown output format %q. Must be either 'text' or 'krm'", r.output)
    96  	}
    97  	return nil
    98  }
    99  
   100  func (r *Runner) RunE(c *cobra.Command, args []string) error {
   101  	// default to the current working directory if the user didn't
   102  	// provide a target package.
   103  	if len(args) == 0 {
   104  		cwd, err := os.Getwd()
   105  		if err != nil {
   106  			return err
   107  		}
   108  		args = append(args, cwd)
   109  	}
   110  
   111  	// Handle symlinks.
   112  	path := args[0]
   113  	var err error
   114  	if args[0] != "-" {
   115  		path, err = argutil.ResolveSymlink(r.ctx, path)
   116  		if err != nil {
   117  			return err
   118  		}
   119  	}
   120  
   121  	// Load the resources from disk or stdin and extract the
   122  	// inventory information.
   123  	objs, inv, err := live.Load(r.factory, path, c.InOrStdin())
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	// Convert the inventory data input to the format required by
   129  	// the actuation code.
   130  	invInfo, err := live.ToInventoryInfo(inv)
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	// Create and execute the planner.
   136  	planner, err := kptplanner.NewClusterPlanner(r.factory)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	plan, err := planner.BuildPlan(r.ctx, invInfo, objs, kptplanner.Options{
   141  		ServerSideOptions: r.serverSideOptions,
   142  	})
   143  	if err != nil {
   144  		return err
   145  	}
   146  
   147  	switch r.output {
   148  	case "text":
   149  		return printText(plan, objs, r.ioStreams)
   150  	case "krm":
   151  		return printKRM(plan, r.ioStreams)
   152  	}
   153  	return fmt.Errorf("unknown output format %s", r.output)
   154  }
   155  
   156  func printText(plan *kptplanner.Plan, objs []*unstructured.Unstructured, ioStreams genericclioptions.IOStreams) error {
   157  	if !hasChanges(plan) {
   158  		fmt.Fprint(ioStreams.Out, "no changes found\n")
   159  		return nil
   160  	}
   161  
   162  	fmt.Fprintf(ioStreams.Out, "kpt will perform the following actions:\n")
   163  	for i := range plan.Actions {
   164  		action := plan.Actions[i]
   165  		switch action.Type {
   166  		case kptplanner.Create:
   167  			printEntryWithColor("+", print.GREEN, action, ioStreams)
   168  			u, ok := findResource(objs, action.Group, action.Kind, action.Namespace, action.Name)
   169  			if !ok {
   170  				panic("can't find resource")
   171  			}
   172  			printKRMWithPrefix(u, ContentPrefix, ioStreams)
   173  		case kptplanner.Unchanged:
   174  			// Do nothing.
   175  		case kptplanner.Delete:
   176  			printEntryWithColor("-", print.RED, action, ioStreams)
   177  		case kptplanner.Update:
   178  			printEntry(" ", action, ioStreams)
   179  			findAndPrintDiff(action.Original, action.Updated, ContentPrefix, ioStreams)
   180  		case kptplanner.Skip:
   181  			// TODO: provide more information about why the resource was skipped.
   182  			printEntryWithColor("=", print.YELLOW, action, ioStreams)
   183  		case kptplanner.Error:
   184  			printEntry("!", action, ioStreams)
   185  			printWithPrefix(action.Error, ContentPrefix, ioStreams)
   186  		}
   187  		fmt.Fprintf(ioStreams.Out, "\n")
   188  	}
   189  	return nil
   190  }
   191  
   192  func hasChanges(plan *kptplanner.Plan) bool {
   193  	for _, a := range plan.Actions {
   194  		if a.Type != kptplanner.Unchanged {
   195  			return true
   196  		}
   197  	}
   198  	return false
   199  }
   200  
   201  func printEntryWithColor(prefix string, color print.Color, action kptplanner.Action, ioStreams genericclioptions.IOStreams) {
   202  	txt := print.SprintfWithColor(color, "%s%s %s/%s %s/%s\n", EntryPrefix, prefix, action.Group, action.Kind, action.Namespace, action.Name)
   203  	fmt.Fprint(ioStreams.Out, txt)
   204  }
   205  
   206  func printEntry(prefix string, action kptplanner.Action, ioStreams genericclioptions.IOStreams) {
   207  	fmt.Fprintf(ioStreams.Out, "%s%s %s/%s %s/%s\n", EntryPrefix, prefix, action.Group, action.Kind, action.Namespace, action.Name)
   208  }
   209  
   210  func findAndPrintDiff(before, after *unstructured.Unstructured, prefix string, ioStreams genericclioptions.IOStreams) {
   211  	diff, err := diffObjects(before, after)
   212  	if err != nil {
   213  		panic(err)
   214  	}
   215  	for _, d := range diff {
   216  		if d.Path == ".metadata.generation" || d.Path == ".metadata.managedFields.0.time" {
   217  			continue
   218  		}
   219  
   220  		switch d.Type {
   221  		case "LeftAdd":
   222  			txt := print.SprintfWithColor(print.RED, "%s-%s: %s\n", prefix, d.Path, strings.TrimSpace(fmt.Sprintf("%v", d.Left)))
   223  			fmt.Fprint(ioStreams.Out, txt)
   224  		case "RightAdd":
   225  			txt := print.SprintfWithColor(print.GREEN, "%s+%s: %s\n", prefix, d.Path, strings.TrimSpace(fmt.Sprintf("%v", d.Right)))
   226  			fmt.Fprint(ioStreams.Out, txt)
   227  		case "Change":
   228  			txt1 := print.SprintfWithColor(print.RED, "%s-%s: %s\n", prefix, d.Path, strings.TrimSpace(fmt.Sprintf("%v", d.Left)))
   229  			fmt.Fprint(ioStreams.Out, txt1)
   230  			txt2 := print.SprintfWithColor(print.GREEN, "%s+%s: %s\n", prefix, d.Path, strings.TrimSpace(fmt.Sprintf("%v", d.Right)))
   231  			fmt.Fprint(ioStreams.Out, txt2)
   232  		}
   233  	}
   234  }
   235  
   236  func findResource(objs []*unstructured.Unstructured, group, kind, namespace, name string) (*unstructured.Unstructured, bool) {
   237  	for i := range objs {
   238  		o := objs[i]
   239  		gvk := o.GroupVersionKind()
   240  		if gvk.Group == group && gvk.Kind == kind && o.GetName() == name && o.GetNamespace() == namespace {
   241  			return o, true
   242  		}
   243  	}
   244  	return nil, false
   245  }
   246  
   247  func printKRMWithPrefix(u *unstructured.Unstructured, prefix string, ioStreams genericclioptions.IOStreams) {
   248  	b, err := yaml.Marshal(u.Object)
   249  	if err != nil {
   250  		panic(fmt.Errorf("unable to marshal resource: %v", err))
   251  	}
   252  	printWithPrefix(string(b), prefix, ioStreams)
   253  }
   254  
   255  func printWithPrefix(text, prefix string, ioStreams genericclioptions.IOStreams) {
   256  	scanner := bufio.NewScanner(strings.NewReader(text))
   257  	for scanner.Scan() {
   258  		fmt.Fprintf(ioStreams.Out, "%s%s\n", prefix, scanner.Text())
   259  	}
   260  	if err := scanner.Err(); err != nil {
   261  		panic(fmt.Errorf("error reading text: %v", err))
   262  	}
   263  }
   264  
   265  // printKRM outputs the plan inside a ResourceList so the output format
   266  // follows the KRM function wire format.
   267  func printKRM(
   268  	plan *kptplanner.Plan,
   269  	ioStreams genericclioptions.IOStreams,
   270  ) error {
   271  	planResource, err := yaml.Parse(strings.TrimSpace(`
   272  apiVersion: kpt.dev/v1alpha1
   273  kind: Plan
   274  metadata:
   275    name: plan
   276    annotations:
   277      config.kubernetes.io/local-config: true	
   278  `))
   279  	if err != nil {
   280  		return fmt.Errorf("unable to create yaml document: %w", err)
   281  	}
   282  
   283  	sNode, err := planResource.Pipe(yaml.LookupCreate(yaml.SequenceNode, "spec", "actions"))
   284  	if err != nil {
   285  		return fmt.Errorf("unable to update yaml document: %w", err)
   286  	}
   287  
   288  	for i := range plan.Actions {
   289  		action := plan.Actions[i]
   290  		a := yaml.NewRNode(&yaml.Node{Kind: yaml.MappingNode})
   291  		fields := map[string]*yaml.RNode{
   292  			"action":     yaml.NewScalarRNode(string(action.Type)),
   293  			"apiVersion": yaml.NewScalarRNode(action.Group),
   294  			"kind":       yaml.NewScalarRNode(action.Kind),
   295  			"name":       yaml.NewScalarRNode(action.Name),
   296  			"namespace":  yaml.NewScalarRNode(action.Namespace),
   297  		}
   298  		if action.Original != nil {
   299  			r, err := unstructuredToRNode(action.Original)
   300  			if err != nil {
   301  				return err
   302  			}
   303  			fields["original"] = r
   304  		}
   305  		if action.Updated != nil {
   306  			r, err := unstructuredToRNode(action.Updated)
   307  			if err != nil {
   308  				return err
   309  			}
   310  			fields["updated"] = r
   311  		}
   312  		if action.Error != "" {
   313  			fields["error"] = yaml.NewScalarRNode(action.Error)
   314  		}
   315  
   316  		for key, val := range fields {
   317  			if err := a.PipeE(yaml.SetField(key, val)); err != nil {
   318  				return fmt.Errorf("unable to update yaml document: %w", err)
   319  			}
   320  		}
   321  		if err := sNode.PipeE(yaml.Append(a.YNode())); err != nil {
   322  			return fmt.Errorf("unable to update yaml document: %w", err)
   323  		}
   324  	}
   325  
   326  	writer := &kio.ByteWriter{
   327  		Writer:                ioStreams.Out,
   328  		KeepReaderAnnotations: true,
   329  		WrappingAPIVersion:    kio.ResourceListAPIVersion,
   330  		WrappingKind:          kio.ResourceListKind,
   331  	}
   332  	err = writer.Write([]*yaml.RNode{planResource})
   333  	if err != nil {
   334  		return fmt.Errorf("failed to write resources: %w", err)
   335  	}
   336  	return nil
   337  }
   338  
   339  func unstructuredToRNode(u *unstructured.Unstructured) (*yaml.RNode, error) {
   340  	b, err := yaml.Marshal(u.Object)
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  	return yaml.Parse((string(b)))
   345  }