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] }