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 }