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