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