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