github.com/wmuizelaar/kpt@v0.0.0-20221018115725-bd564717b2ed/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 //nolint:unused 132 type node struct { 133 p TreeWriter 134 *yaml.RNode 135 children []*node 136 } 137 138 //nolint:unused 139 func (a node) Len() int { return len(a.children) } 140 141 //nolint:unused 142 func (a node) Swap(i, j int) { a.children[i], a.children[j] = a.children[j], a.children[i] } 143 144 //nolint:unused 145 func (a node) Less(i, j int) bool { 146 return compareNodes(a.children[i].RNode, a.children[j].RNode) 147 } 148 149 // Tree adds this node to the root 150 //nolint:unused 151 func (a node) Tree(root treeprint.Tree) error { 152 sort.Sort(a) 153 branch := root 154 var err error 155 156 // generate a node for the Resource 157 if a.RNode != nil { 158 branch, err = a.p.doResource(a.RNode, "Resource", root) 159 if err != nil { 160 return err 161 } 162 } 163 164 // attach children to the branch 165 for _, n := range a.children { 166 if err := n.Tree(branch); err != nil { 167 return err 168 } 169 } 170 return nil 171 } 172 173 // index indexes the Resources by their package 174 func (p TreeWriter) index(nodes []*yaml.RNode) map[string][]*yaml.RNode { 175 // index the ResourceNodes by package 176 indexByPackage := map[string][]*yaml.RNode{} 177 for i := range nodes { 178 err := kioutil.CopyLegacyAnnotations(nodes[i]) 179 if err != nil { 180 continue 181 } 182 meta, err := nodes[i].GetMeta() 183 if err != nil || meta.Kind == "" { 184 // not a resource 185 continue 186 } 187 pkg := filepath.Dir(meta.Annotations[kioutil.PathAnnotation]) 188 indexByPackage[pkg] = append(indexByPackage[pkg], nodes[i]) 189 } 190 return indexByPackage 191 } 192 193 func compareNodes(i, j *yaml.RNode) bool { 194 _ = kioutil.CopyLegacyAnnotations(i) 195 _ = kioutil.CopyLegacyAnnotations(j) 196 197 metai, _ := i.GetMeta() 198 metaj, _ := j.GetMeta() 199 pi := metai.Annotations[kioutil.PathAnnotation] 200 pj := metaj.Annotations[kioutil.PathAnnotation] 201 202 // compare file names 203 if filepath.Base(pi) != filepath.Base(pj) { 204 return filepath.Base(pi) < filepath.Base(pj) 205 } 206 207 // compare namespace 208 if metai.Namespace != metaj.Namespace { 209 return metai.Namespace < metaj.Namespace 210 } 211 212 // compare name 213 if metai.Name != metaj.Name { 214 return metai.Name < metaj.Name 215 } 216 217 // compare kind 218 if metai.Kind != metaj.Kind { 219 return metai.Kind < metaj.Kind 220 } 221 222 // compare apiVersion 223 if metai.APIVersion != metaj.APIVersion { 224 return metai.APIVersion < metaj.APIVersion 225 } 226 return true 227 } 228 229 // sort sorts the Resources in the index in display order and returns the ordered 230 // keys for the index 231 // 232 // Packages are sorted by package name 233 // Resources within a package are sorted by: [filename, namespace, name, kind, apiVersion] 234 func (p TreeWriter) sort(indexByPackage map[string][]*yaml.RNode) []string { 235 var keys []string 236 for k := range indexByPackage { 237 pkgNodes := indexByPackage[k] 238 sort.Slice(pkgNodes, func(i, j int) bool { return compareNodes(pkgNodes[i], pkgNodes[j]) }) 239 keys = append(keys, k) 240 } 241 242 // return the package names sorted lexicographically 243 sort.Strings(keys) 244 return keys 245 } 246 247 func (p TreeWriter) doResource(leaf *yaml.RNode, metaString string, branch treeprint.Tree) (treeprint.Tree, error) { 248 err := kioutil.CopyLegacyAnnotations(leaf) 249 if err != nil { 250 return nil, err 251 } 252 meta, _ := leaf.GetMeta() 253 if metaString == "" { 254 path := meta.Annotations[kioutil.PathAnnotation] 255 path = filepath.Base(path) 256 metaString = path 257 } 258 259 value := fmt.Sprintf("%s %s", meta.Kind, meta.Name) 260 if len(meta.Namespace) > 0 { 261 value = fmt.Sprintf("%s %s/%s", meta.Kind, meta.Namespace, meta.Name) 262 } 263 264 fields, err := p.getFields(leaf) 265 if err != nil { 266 return nil, err 267 } 268 269 n := branch.AddMetaBranch(metaString, value) 270 for i := range fields { 271 field := fields[i] 272 273 // do leaf node 274 if len(field.matchingElementsAndFields) == 0 { 275 n.AddNode(fmt.Sprintf("%s: %s", field.name, field.value)) 276 continue 277 } 278 279 // do nested nodes 280 b := n.AddBranch(field.name) 281 for j := range field.matchingElementsAndFields { 282 elem := field.matchingElementsAndFields[j] 283 b := b.AddBranch(elem.name) 284 for k := range elem.matchingElementsAndFields { 285 field := elem.matchingElementsAndFields[k] 286 b.AddNode(fmt.Sprintf("%s: %s", field.name, field.value)) 287 } 288 } 289 } 290 291 return n, nil 292 } 293 294 // getFields looks up p.Fields from leaf and structures them into treeFields. 295 // TODO(pwittrock): simplify this function 296 func (p TreeWriter) getFields(leaf *yaml.RNode) (treeFields, error) { 297 fieldsByName := map[string]*treeField{} 298 299 // index nested and non-nested fields 300 for i := range p.Fields { 301 f := p.Fields[i] 302 seq, err := leaf.Pipe(&f) 303 if err != nil { 304 return nil, err 305 } 306 if seq == nil { 307 continue 308 } 309 310 if fieldsByName[f.Name] == nil { 311 fieldsByName[f.Name] = &treeField{name: f.Name} 312 } 313 314 // non-nested field -- add directly to the treeFields list 315 if f.SubName == "" { 316 // non-nested field -- only 1 element 317 val, err := yaml.String(seq.Content()[0], yaml.Trim, yaml.Flow) 318 if err != nil { 319 return nil, err 320 } 321 fieldsByName[f.Name].value = val 322 continue 323 } 324 325 // nested-field -- create a parent elem, and index by the 'match' value 326 if fieldsByName[f.Name].subFieldByMatch == nil { 327 fieldsByName[f.Name].subFieldByMatch = map[string]treeFields{} 328 } 329 index := fieldsByName[f.Name].subFieldByMatch 330 for j := range seq.Content() { 331 elem := seq.Content()[j] 332 matches := f.Matches[elem] 333 str, err := yaml.String(elem, yaml.Trim, yaml.Flow) 334 if err != nil { 335 return nil, err 336 } 337 338 // map the field by the name of the element 339 // index the subfields by the matching element so we can put all the fields for the 340 // same element under the same branch 341 matchKey := strings.Join(matches, "/") 342 index[matchKey] = append(index[matchKey], &treeField{name: f.SubName, value: str}) 343 } 344 } 345 346 // iterate over collection of all queried fields in the Resource 347 for _, field := range fieldsByName { 348 // iterate over collection of elements under the field -- indexed by element name 349 for match, subFields := range field.subFieldByMatch { 350 // create a new element for this collection of fields 351 // note: we will convert name to an index later, but keep the match for sorting 352 elem := &treeField{name: match} 353 field.matchingElementsAndFields = append(field.matchingElementsAndFields, elem) 354 355 // iterate over collection of queried fields for the element 356 for i := range subFields { 357 // add to the list of fields for this element 358 elem.matchingElementsAndFields = append(elem.matchingElementsAndFields, subFields[i]) 359 } 360 } 361 // clear this cached data 362 field.subFieldByMatch = nil 363 } 364 365 // put the fields in a list so they are ordered 366 fieldList := treeFields{} 367 for _, v := range fieldsByName { 368 fieldList = append(fieldList, v) 369 } 370 371 // sort the fields 372 sort.Sort(fieldList) 373 for i := range fieldList { 374 field := fieldList[i] 375 // sort the elements under this field 376 sort.Sort(field.matchingElementsAndFields) 377 378 for i := range field.matchingElementsAndFields { 379 element := field.matchingElementsAndFields[i] 380 // sort the elements under a list field by their name 381 sort.Sort(element.matchingElementsAndFields) 382 // set the name of the element to its index 383 element.name = fmt.Sprintf("%d", i) 384 } 385 } 386 387 return fieldList, nil 388 } 389 390 // treeField wraps a field node 391 type treeField struct { 392 // name is the name of the node 393 name string 394 395 // value is the value of the node -- may be empty 396 value string 397 398 // matchingElementsAndFields is a slice of fields that go under this as a branch 399 matchingElementsAndFields treeFields 400 401 // subFieldByMatch caches matchingElementsAndFields indexed by the name of the matching elem 402 subFieldByMatch map[string]treeFields 403 } 404 405 // treeFields wraps a slice of treeField so they can be sorted 406 type treeFields []*treeField 407 408 func (nodes treeFields) Len() int { return len(nodes) } 409 410 func (nodes treeFields) Less(i, j int) bool { 411 iIndex, iFound := yaml.FieldOrder[nodes[i].name] 412 jIndex, jFound := yaml.FieldOrder[nodes[j].name] 413 if iFound && jFound { 414 return iIndex < jIndex 415 } 416 if iFound { 417 return true 418 } 419 if jFound { 420 return false 421 } 422 423 if nodes[i].name != nodes[j].name { 424 return nodes[i].name < nodes[j].name 425 } 426 if nodes[i].value != nodes[j].value { 427 return nodes[i].value < nodes[j].value 428 } 429 return false 430 } 431 432 func (nodes treeFields) Swap(i, j int) { nodes[i], nodes[j] = nodes[j], nodes[i] }