1// Copyright 2019 The Kubernetes Authors. 2// SPDX-License-Identifier: Apache-2.0 3 4package kio 5 6import ( 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "sort" 12 "strings" 13 14 "github.com/xlab/treeprint" 15 "sigs.k8s.io/kustomize/kyaml/kio/kioutil" 16 "sigs.k8s.io/kustomize/kyaml/yaml" 17) 18 19type TreeStructure string 20 21const ( 22 // TreeStructurePackage configures TreeWriter to generate the tree structure off of the 23 // Resources packages. 24 TreeStructurePackage TreeStructure = "directory" 25 26 // TreeStructureOwners configures TreeWriter to generate the tree structure off of the 27 // Resource owners. 28 TreeStructureGraph TreeStructure = "owners" 29) 30 31var GraphStructures = []string{string(TreeStructureGraph), string(TreeStructurePackage)} 32 33// TreeWriter prints the package structured as a tree. 34// TODO(pwittrock): test this package better. it is lower-risk since it is only 35// used for printing rather than updating or editing. 36type TreeWriter struct { 37 Writer io.Writer 38 Root string 39 Fields []TreeWriterField 40 Structure TreeStructure 41 OpenAPIFileName string 42} 43 44// TreeWriterField configures a Resource field to be included in the tree 45type TreeWriterField struct { 46 yaml.PathMatcher 47 Name string 48 SubName string 49} 50 51func (p TreeWriter) packageStructure(nodes []*yaml.RNode) error { 52 indexByPackage := p.index(nodes) 53 54 // create the new tree 55 tree := treeprint.New() 56 tree.SetValue(p.Root) 57 58 // add each package to the tree 59 treeIndex := map[string]treeprint.Tree{} 60 keys := p.sort(indexByPackage) 61 for _, pkg := range keys { 62 // create a branch for this package -- search for the parent package and create 63 // the branch under it -- requires that the keys are sorted 64 branch := tree 65 for parent, subTree := range treeIndex { 66 if strings.HasPrefix(pkg, parent) { 67 // found a package whose path is a prefix to our own, use this 68 // package if a closer one isn't found 69 branch = subTree 70 // don't break, continue searching for more closely related ancestors 71 } 72 } 73 74 // create a new branch for the package 75 createOk := pkg != "." // special edge case logic for tree on current working dir 76 if createOk { 77 branch = branch.AddBranch(branchName(p.Root, pkg, p.OpenAPIFileName)) 78 } 79 80 // cache the branch for this package 81 treeIndex[pkg] = branch 82 83 // print each resource in the package 84 for i := range indexByPackage[pkg] { 85 var err error 86 if _, err = p.doResource(indexByPackage[pkg][i], "", branch); err != nil { 87 return err 88 } 89 } 90 } 91 92 _, err := io.WriteString(p.Writer, tree.String()) 93 return err 94} 95 96// branchName takes the root directory and relative path to the directory 97// and returns the branch name 98func branchName(root, dirRelPath, openAPIFileName string) string { 99 name := filepath.Base(dirRelPath) 100 _, err := os.Stat(filepath.Join(root, dirRelPath, openAPIFileName)) 101 if !os.IsNotExist(err) { 102 // add Pkg: prefix indicating that it is a separate package as it has 103 // openAPIFile 104 return fmt.Sprintf("Pkg: %s", name) 105 } 106 return name 107} 108 109// Write writes the ascii tree to p.Writer 110func (p TreeWriter) Write(nodes []*yaml.RNode) error { 111 switch p.Structure { 112 case TreeStructurePackage: 113 return p.packageStructure(nodes) 114 case TreeStructureGraph: 115 return p.graphStructure(nodes) 116 } 117 118 // If any resource has an owner reference, default to the graph structure. Otherwise, use package structure. 119 for _, node := range nodes { 120 if owners, _ := node.Pipe(yaml.Lookup("metadata", "ownerReferences")); owners != nil { 121 return p.graphStructure(nodes) 122 } 123 } 124 return p.packageStructure(nodes) 125} 126 127// node wraps a tree node, and any children nodes 128type node struct { 129 p TreeWriter 130 *yaml.RNode 131 children []*node 132} 133 134func (a node) Len() int { return len(a.children) } 135func (a node) Swap(i, j int) { a.children[i], a.children[j] = a.children[j], a.children[i] } 136func (a node) Less(i, j int) bool { 137 return compareNodes(a.children[i].RNode, a.children[j].RNode) 138} 139 140// Tree adds this node to the root 141func (a node) Tree(root treeprint.Tree) error { 142 sort.Sort(a) 143 branch := root 144 var err error 145 146 // generate a node for the Resource 147 if a.RNode != nil { 148 branch, err = a.p.doResource(a.RNode, "Resource", root) 149 if err != nil { 150 return err 151 } 152 } 153 154 // attach children to the branch 155 for _, n := range a.children { 156 if err := n.Tree(branch); err != nil { 157 return err 158 } 159 } 160 return nil 161} 162 163// graphStructure writes the tree using owners for structure 164func (p TreeWriter) graphStructure(nodes []*yaml.RNode) error { 165 resourceToOwner := map[string]*node{} 166 root := &node{} 167 // index each of the nodes by their owner 168 for _, n := range nodes { 169 ownerVal, err := ownerToString(n) 170 if err != nil { 171 return err 172 } 173 var owner *node 174 if ownerVal == "" { 175 // no owner -- attach to the root 176 owner = root 177 } else { 178 // owner found -- attach to the owner 179 var found bool 180 owner, found = resourceToOwner[ownerVal] 181 if !found { 182 // initialize the owner if not found 183 resourceToOwner[ownerVal] = &node{p: p} 184 owner = resourceToOwner[ownerVal] 185 } 186 } 187 188 nodeVal, err := nodeToString(n) 189 if err != nil { 190 return err 191 } 192 val, found := resourceToOwner[nodeVal] 193 if !found { 194 // initialize the node if not found -- may have already been initialized if it 195 // is the owner of another node 196 resourceToOwner[nodeVal] = &node{p: p} 197 val = resourceToOwner[nodeVal] 198 } 199 val.RNode = n 200 owner.children = append(owner.children, val) 201 } 202 203 for k, v := range resourceToOwner { 204 if v.RNode == nil { 205 return fmt.Errorf( 206 "owner '%s' not found in input, but found as an owner of input objects", k) 207 } 208 } 209 210 // print the tree 211 tree := treeprint.New() 212 if err := root.Tree(tree); err != nil { 213 return err 214 } 215 216 _, err := io.WriteString(p.Writer, tree.String()) 217 return err 218} 219 220// nodeToString generates a string to identify the node -- matches ownerToString format 221func nodeToString(node *yaml.RNode) (string, error) { 222 meta, err := node.GetMeta() 223 if err != nil { 224 return "", err 225 } 226 227 return fmt.Sprintf("%s %s/%s", meta.Kind, meta.Namespace, meta.Name), nil 228} 229 230// ownerToString generate a string to identify the owner -- matches nodeToString format 231func ownerToString(node *yaml.RNode) (string, error) { 232 meta, err := node.GetMeta() 233 if err != nil { 234 return "", err 235 } 236 namespace := meta.Namespace 237 238 owners, err := node.Pipe(yaml.Lookup("metadata", "ownerReferences")) 239 if err != nil { 240 return "", err 241 } 242 if owners == nil { 243 return "", nil 244 } 245 246 elements, err := owners.Elements() 247 if err != nil { 248 return "", err 249 } 250 if len(elements) == 0 { 251 return "", err 252 } 253 owner := elements[0] 254 var kind, name string 255 256 if value := owner.Field("kind"); !value.IsNilOrEmpty() { 257 kind = value.Value.YNode().Value 258 } 259 if value := owner.Field("name"); !value.IsNilOrEmpty() { 260 name = value.Value.YNode().Value 261 } 262 263 return fmt.Sprintf("%s %s/%s", kind, namespace, name), nil 264} 265 266// index indexes the Resources by their package 267func (p TreeWriter) index(nodes []*yaml.RNode) map[string][]*yaml.RNode { 268 // index the ResourceNodes by package 269 indexByPackage := map[string][]*yaml.RNode{} 270 for i := range nodes { 271 meta, err := nodes[i].GetMeta() 272 if err != nil || meta.Kind == "" { 273 // not a resource 274 continue 275 } 276 pkg := filepath.Dir(meta.Annotations[kioutil.PathAnnotation]) 277 indexByPackage[pkg] = append(indexByPackage[pkg], nodes[i]) 278 } 279 return indexByPackage 280} 281 282func compareNodes(i, j *yaml.RNode) bool { 283 metai, _ := i.GetMeta() 284 metaj, _ := j.GetMeta() 285 pi := metai.Annotations[kioutil.PathAnnotation] 286 pj := metaj.Annotations[kioutil.PathAnnotation] 287 288 // compare file names 289 if filepath.Base(pi) != filepath.Base(pj) { 290 return filepath.Base(pi) < filepath.Base(pj) 291 } 292 293 // compare namespace 294 if metai.Namespace != metaj.Namespace { 295 return metai.Namespace < metaj.Namespace 296 } 297 298 // compare name 299 if metai.Name != metaj.Name { 300 return metai.Name < metaj.Name 301 } 302 303 // compare kind 304 if metai.Kind != metaj.Kind { 305 return metai.Kind < metaj.Kind 306 } 307 308 // compare apiVersion 309 if metai.APIVersion != metaj.APIVersion { 310 return metai.APIVersion < metaj.APIVersion 311 } 312 return true 313} 314 315// sort sorts the Resources in the index in display order and returns the ordered 316// keys for the index 317// 318// Packages are sorted by package name 319// Resources within a package are sorted by: [filename, namespace, name, kind, apiVersion] 320func (p TreeWriter) sort(indexByPackage map[string][]*yaml.RNode) []string { 321 var keys []string 322 for k := range indexByPackage { 323 pkgNodes := indexByPackage[k] 324 sort.Slice(pkgNodes, func(i, j int) bool { return compareNodes(pkgNodes[i], pkgNodes[j]) }) 325 keys = append(keys, k) 326 } 327 328 // return the package names sorted lexicographically 329 sort.Strings(keys) 330 return keys 331} 332 333func (p TreeWriter) doResource(leaf *yaml.RNode, metaString string, branch treeprint.Tree) (treeprint.Tree, error) { 334 meta, _ := leaf.GetMeta() 335 if metaString == "" { 336 path := meta.Annotations[kioutil.PathAnnotation] 337 path = filepath.Base(path) 338 metaString = path 339 } 340 341 value := fmt.Sprintf("%s %s", meta.Kind, meta.Name) 342 if len(meta.Namespace) > 0 { 343 value = fmt.Sprintf("%s %s/%s", meta.Kind, meta.Namespace, meta.Name) 344 } 345 346 fields, err := p.getFields(leaf) 347 if err != nil { 348 return nil, err 349 } 350 351 n := branch.AddMetaBranch(metaString, value) 352 for i := range fields { 353 field := fields[i] 354 355 // do leaf node 356 if len(field.matchingElementsAndFields) == 0 { 357 n.AddNode(fmt.Sprintf("%s: %s", field.name, field.value)) 358 continue 359 } 360 361 // do nested nodes 362 b := n.AddBranch(field.name) 363 for j := range field.matchingElementsAndFields { 364 elem := field.matchingElementsAndFields[j] 365 b := b.AddBranch(elem.name) 366 for k := range elem.matchingElementsAndFields { 367 field := elem.matchingElementsAndFields[k] 368 b.AddNode(fmt.Sprintf("%s: %s", field.name, field.value)) 369 } 370 } 371 } 372 373 return n, nil 374} 375 376// getFields looks up p.Fields from leaf and structures them into treeFields. 377// TODO(pwittrock): simplify this function 378func (p TreeWriter) getFields(leaf *yaml.RNode) (treeFields, error) { 379 fieldsByName := map[string]*treeField{} 380 381 // index nested and non-nested fields 382 for i := range p.Fields { 383 f := p.Fields[i] 384 seq, err := leaf.Pipe(&f) 385 if err != nil { 386 return nil, err 387 } 388 if seq == nil { 389 continue 390 } 391 392 if fieldsByName[f.Name] == nil { 393 fieldsByName[f.Name] = &treeField{name: f.Name} 394 } 395 396 // non-nested field -- add directly to the treeFields list 397 if f.SubName == "" { 398 // non-nested field -- only 1 element 399 val, err := yaml.String(seq.Content()[0], yaml.Trim, yaml.Flow) 400 if err != nil { 401 return nil, err 402 } 403 fieldsByName[f.Name].value = val 404 continue 405 } 406 407 // nested-field -- create a parent elem, and index by the 'match' value 408 if fieldsByName[f.Name].subFieldByMatch == nil { 409 fieldsByName[f.Name].subFieldByMatch = map[string]treeFields{} 410 } 411 index := fieldsByName[f.Name].subFieldByMatch 412 for j := range seq.Content() { 413 elem := seq.Content()[j] 414 matches := f.Matches[elem] 415 str, err := yaml.String(elem, yaml.Trim, yaml.Flow) 416 if err != nil { 417 return nil, err 418 } 419 420 // map the field by the name of the element 421 // index the subfields by the matching element so we can put all the fields for the 422 // same element under the same branch 423 matchKey := strings.Join(matches, "/") 424 index[matchKey] = append(index[matchKey], &treeField{name: f.SubName, value: str}) 425 } 426 } 427 428 // iterate over collection of all queried fields in the Resource 429 for _, field := range fieldsByName { 430 // iterate over collection of elements under the field -- indexed by element name 431 for match, subFields := range field.subFieldByMatch { 432 // create a new element for this collection of fields 433 // note: we will convert name to an index later, but keep the match for sorting 434 elem := &treeField{name: match} 435 field.matchingElementsAndFields = append(field.matchingElementsAndFields, elem) 436 437 // iterate over collection of queried fields for the element 438 for i := range subFields { 439 // add to the list of fields for this element 440 elem.matchingElementsAndFields = append(elem.matchingElementsAndFields, subFields[i]) 441 } 442 } 443 // clear this cached data 444 field.subFieldByMatch = nil 445 } 446 447 // put the fields in a list so they are ordered 448 fieldList := treeFields{} 449 for _, v := range fieldsByName { 450 fieldList = append(fieldList, v) 451 } 452 453 // sort the fields 454 sort.Sort(fieldList) 455 for i := range fieldList { 456 field := fieldList[i] 457 // sort the elements under this field 458 sort.Sort(field.matchingElementsAndFields) 459 460 for i := range field.matchingElementsAndFields { 461 element := field.matchingElementsAndFields[i] 462 // sort the elements under a list field by their name 463 sort.Sort(element.matchingElementsAndFields) 464 // set the name of the element to its index 465 element.name = fmt.Sprintf("%d", i) 466 } 467 } 468 469 return fieldList, nil 470} 471 472// treeField wraps a field node 473type treeField struct { 474 // name is the name of the node 475 name string 476 477 // value is the value of the node -- may be empty 478 value string 479 480 // matchingElementsAndFields is a slice of fields that go under this as a branch 481 matchingElementsAndFields treeFields 482 483 // subFieldByMatch caches matchingElementsAndFields indexed by the name of the matching elem 484 subFieldByMatch map[string]treeFields 485} 486 487// treeFields wraps a slice of treeField so they can be sorted 488type treeFields []*treeField 489 490func (nodes treeFields) Len() int { return len(nodes) } 491 492func (nodes treeFields) Less(i, j int) bool { 493 iIndex, iFound := yaml.FieldOrder[nodes[i].name] 494 jIndex, jFound := yaml.FieldOrder[nodes[j].name] 495 if iFound && jFound { 496 return iIndex < jIndex 497 } 498 if iFound { 499 return true 500 } 501 if jFound { 502 return false 503 } 504 505 if nodes[i].name != nodes[j].name { 506 return nodes[i].name < nodes[j].name 507 } 508 if nodes[i].value != nodes[j].value { 509 return nodes[i].value < nodes[j].value 510 } 511 return false 512} 513 514func (nodes treeFields) Swap(i, j int) { nodes[i], nodes[j] = nodes[j], nodes[i] } 515