From 80ef184b60874c5f5fbe8fce35f8b6206920ac7b Mon Sep 17 00:00:00 2001
From: Stefan Prodan <stefan.prodan@gmail.com>
Date: Sat, 23 Oct 2021 11:04:31 +0300
Subject: [PATCH] Add flux tree command The `flux tree kustomization` command
 prints the resources reconciled by the given Kustomization.

Signed-off-by: Stefan Prodan <stefan.prodan@gmail.com>
---
 cmd/flux/testdata/tree/kustomizations.yaml |  89 +++++++++++++
 cmd/flux/testdata/tree/tree-compact.golden |   6 +
 cmd/flux/testdata/tree/tree-empty.golden   |   2 +
 cmd/flux/testdata/tree/tree.golden         |  12 ++
 cmd/flux/tree.go                           |  31 +++++
 cmd/flux/tree_kustomization.go             | 138 ++++++++++++++++++++
 cmd/flux/tree_kustomization_test.go        |  64 ++++++++++
 internal/tree/tree.go                      | 141 +++++++++++++++++++++
 8 files changed, 483 insertions(+)
 create mode 100644 cmd/flux/testdata/tree/kustomizations.yaml
 create mode 100644 cmd/flux/testdata/tree/tree-compact.golden
 create mode 100644 cmd/flux/testdata/tree/tree-empty.golden
 create mode 100644 cmd/flux/testdata/tree/tree.golden
 create mode 100644 cmd/flux/tree.go
 create mode 100644 cmd/flux/tree_kustomization.go
 create mode 100644 cmd/flux/tree_kustomization_test.go
 create mode 100644 internal/tree/tree.go

diff --git a/cmd/flux/testdata/tree/kustomizations.yaml b/cmd/flux/testdata/tree/kustomizations.yaml
new file mode 100644
index 00000000..a5739d6f
--- /dev/null
+++ b/cmd/flux/testdata/tree/kustomizations.yaml
@@ -0,0 +1,89 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: {{ .fluxns }}
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
+kind: Kustomization
+metadata:
+  name: flux-system
+  namespace: {{ .fluxns }}
+spec:
+  path: ./clusters/production
+  sourceRef:
+    kind: GitRepository
+    name: flux-system
+  interval: 5m
+  prune: true
+status:
+  conditions:
+  - lastTransitionTime: "2021-08-01T04:52:56Z"
+    message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f'
+    reason: ReconciliationSucceeded
+    status: "True"
+    type: Ready
+  inventory:
+    entries:
+      - id: _{{ .fluxns }}__Namespace
+        v: v1
+      - id: {{ .fluxns }}_helm-controller_apps_Deployment
+        v: v1
+      - id: {{ .fluxns }}_kustomize-controller_apps_Deployment
+        v: v1
+      - id: {{ .fluxns }}_notification-controller_apps_Deployment
+        v: v1
+      - id: {{ .fluxns }}_source-controller_apps_Deployment
+        v: v1
+      - id: {{ .fluxns }}_infrastructure_kustomize.toolkit.fluxcd.io_Kustomization
+        v: v1beta2
+      - id: {{ .fluxns }}_flux-system_source.toolkit.fluxcd.io_GitRepository
+        v: v1beta1
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
+kind: Kustomization
+metadata:
+  name: infrastructure
+  namespace: {{ .fluxns }}
+spec:
+  path: ./infrastructure/production
+  sourceRef:
+    kind: GitRepository
+    name: flux-system
+  interval: 5m
+  prune: true
+status:
+  conditions:
+    - lastTransitionTime: "2021-08-01T04:52:56Z"
+      message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f'
+      reason: ReconciliationSucceeded
+      status: "True"
+      type: Ready
+  inventory:
+    entries:
+      - id: _cert-manager__Namespace
+        v: v1
+      - id: cert-manager_cert-manager_source.toolkit.fluxcd.io_HelmRepository
+        v: v1beta1
+      - id: cert-manager_cert-manager_helm.toolkit.fluxcd.io_HelmRelease
+        v: v2beta1
+---
+apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
+kind: Kustomization
+metadata:
+  name: empty
+  namespace: {{ .fluxns }}
+spec:
+  path: ./apps/todo
+  sourceRef:
+    kind: GitRepository
+    name: flux-system
+  interval: 5m
+  prune: true
+status:
+  conditions:
+    - lastTransitionTime: "2021-08-01T04:52:56Z"
+      message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f'
+      reason: ReconciliationSucceeded
+      status: "True"
+      type: Ready
diff --git a/cmd/flux/testdata/tree/tree-compact.golden b/cmd/flux/testdata/tree/tree-compact.golden
new file mode 100644
index 00000000..a635f6c8
--- /dev/null
+++ b/cmd/flux/testdata/tree/tree-compact.golden
@@ -0,0 +1,6 @@
+Kustomization/{{ .fluxns }}/flux-system
+├── Kustomization/{{ .fluxns }}/infrastructure
+│   ├── HelmRepository/cert-manager/cert-manager
+│   └── HelmRelease/cert-manager/cert-manager
+└── GitRepository/{{ .fluxns }}/flux-system
+
diff --git a/cmd/flux/testdata/tree/tree-empty.golden b/cmd/flux/testdata/tree/tree-empty.golden
new file mode 100644
index 00000000..6875d696
--- /dev/null
+++ b/cmd/flux/testdata/tree/tree-empty.golden
@@ -0,0 +1,2 @@
+Kustomization/{{ .fluxns }}/empty
+
diff --git a/cmd/flux/testdata/tree/tree.golden b/cmd/flux/testdata/tree/tree.golden
new file mode 100644
index 00000000..e2af2008
--- /dev/null
+++ b/cmd/flux/testdata/tree/tree.golden
@@ -0,0 +1,12 @@
+Kustomization/{{ .fluxns }}/flux-system
+├── Namespace/{{ .fluxns }}
+├── Deployment/{{ .fluxns }}/helm-controller
+├── Deployment/{{ .fluxns }}/kustomize-controller
+├── Deployment/{{ .fluxns }}/notification-controller
+├── Deployment/{{ .fluxns }}/source-controller
+├── Kustomization/{{ .fluxns }}/infrastructure
+│   ├── Namespace/cert-manager
+│   ├── HelmRepository/cert-manager/cert-manager
+│   └── HelmRelease/cert-manager/cert-manager
+└── GitRepository/{{ .fluxns }}/flux-system
+
diff --git a/cmd/flux/tree.go b/cmd/flux/tree.go
new file mode 100644
index 00000000..8e2503c0
--- /dev/null
+++ b/cmd/flux/tree.go
@@ -0,0 +1,31 @@
+/*
+Copyright 2021 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"github.com/spf13/cobra"
+)
+
+var treeCmd = &cobra.Command{
+	Use:   "tree",
+	Short: "Print the resources reconciled by Flux",
+	Long:  `The tree command shows the list of resources reconciled by a Flux object.'`,
+}
+
+func init() {
+	rootCmd.AddCommand(treeCmd)
+}
diff --git a/cmd/flux/tree_kustomization.go b/cmd/flux/tree_kustomization.go
new file mode 100644
index 00000000..8dff24bf
--- /dev/null
+++ b/cmd/flux/tree_kustomization.go
@@ -0,0 +1,138 @@
+/*
+Copyright 2021 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/fluxcd/flux2/internal/tree"
+	"github.com/fluxcd/flux2/internal/utils"
+	kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/cli-utils/pkg/object"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var treeKsCmd = &cobra.Command{
+	Use:     "kustomization [name]",
+	Aliases: []string{"ks", "kustomization"},
+	Short:   "Print the resource inventory of a Kustomization",
+	Long:    `The tree command prints the resource list reconciled by a Kustomization.'`,
+	Example: `  # Print the resources managed by the root Kustomization
+  flux tree kustomization flux-system
+
+  # Print the Flux resources managed by the root Kustomization
+  flux tree kustomization flux-system --compact`,
+	RunE: treeKsCmdRun,
+}
+
+type TreeKsFlags struct {
+	compact bool
+}
+
+var treeKsArgs TreeKsFlags
+
+func init() {
+	treeKsCmd.Flags().BoolVar(&treeKsArgs.compact, "compact", false, "list Flux resources only.")
+	treeCmd.AddCommand(treeKsCmd)
+}
+
+func treeKsCmdRun(cmd *cobra.Command, args []string) error {
+	if len(args) < 1 {
+		return fmt.Errorf("kustomization name is required")
+	}
+	name := args[0]
+
+	ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
+	defer cancel()
+
+	kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext)
+	if err != nil {
+		return err
+	}
+
+	k := &kustomizev1.Kustomization{}
+	err = kubeClient.Get(ctx, client.ObjectKey{
+		Namespace: rootArgs.namespace,
+		Name:      name,
+	}, k)
+	if err != nil {
+		return err
+	}
+
+	kMeta, err := object.CreateObjMetadata(k.Namespace, k.Name,
+		schema.GroupKind{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind})
+	if err != nil {
+		return err
+	}
+
+	kTree := tree.New(kMeta)
+	err = treeKustomization(ctx, kTree, k, kubeClient, treeKsArgs.compact)
+	if err != nil {
+		return err
+	}
+
+	rootCmd.Println(kTree.Print())
+
+	return nil
+}
+
+func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kustomizev1.Kustomization, kubeClient client.Client, compact bool) error {
+	if item.Status.Inventory == nil || len(item.Status.Inventory.Entries) == 0 {
+		return nil
+	}
+
+	for _, entry := range item.Status.Inventory.Entries {
+		objMetadata, err := object.ParseObjMetadata(entry.ID)
+		if err != nil {
+			return err
+		}
+
+		if compact && !strings.Contains(objMetadata.GroupKind.Group, "toolkit.fluxcd.io") {
+			continue
+		}
+
+		if objMetadata.GroupKind.Group == kustomizev1.GroupVersion.Group &&
+			objMetadata.GroupKind.Kind == kustomizev1.KustomizationKind &&
+			objMetadata.Namespace == item.Namespace &&
+			objMetadata.Name == item.Name {
+			continue
+		}
+
+		ks := tree.Add(objMetadata)
+		if objMetadata.GroupKind.Group == kustomizev1.GroupVersion.Group &&
+			objMetadata.GroupKind.Kind == kustomizev1.KustomizationKind {
+			k := &kustomizev1.Kustomization{}
+			err = kubeClient.Get(ctx, client.ObjectKey{
+				Namespace: objMetadata.Namespace,
+				Name:      objMetadata.Name,
+			}, k)
+			if err != nil {
+				return fmt.Errorf("failed to find object: %w", err)
+			}
+			err := treeKustomization(ctx, ks, k, kubeClient, compact)
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/cmd/flux/tree_kustomization_test.go b/cmd/flux/tree_kustomization_test.go
new file mode 100644
index 00000000..be5ce57e
--- /dev/null
+++ b/cmd/flux/tree_kustomization_test.go
@@ -0,0 +1,64 @@
+// +build unit
+
+/*
+Copyright 2021 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"testing"
+)
+
+func TestTree(t *testing.T) {
+	cases := []struct {
+		name       string
+		args       string
+		objectFile string
+		goldenFile string
+	}{
+		{
+			"tree kustomization",
+			"tree kustomization flux-system",
+			"testdata/tree/kustomizations.yaml",
+			"testdata/tree/tree.golden",
+		},
+		{
+			"tree kustomization compact",
+			"tree kustomization flux-system --compact",
+			"testdata/tree/kustomizations.yaml",
+			"testdata/tree/tree-compact.golden",
+		},
+		{
+			"tree kustomization empty",
+			"tree kustomization empty",
+			"testdata/tree/kustomizations.yaml",
+			"testdata/tree/tree-empty.golden",
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			tmpl := map[string]string{
+				"fluxns": allocateNamespace("flux-system"),
+			}
+			testEnv.CreateObjectFile(tc.objectFile, tmpl, t)
+			cmd := cmdTestCase{
+				args:   tc.args + " -n=" + tmpl["fluxns"],
+				assert: assertGoldenTemplateFile(tc.goldenFile, tmpl),
+			}
+			cmd.runTestCmd(t)
+		})
+	}
+}
diff --git a/internal/tree/tree.go b/internal/tree/tree.go
new file mode 100644
index 00000000..d238effb
--- /dev/null
+++ b/internal/tree/tree.go
@@ -0,0 +1,141 @@
+/*
+Copyright 2021 The Flux authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+Derived work from https://github.com/d6o/GoTree
+Copyright (c) 2017 Diego Siqueira
+*/
+
+package tree
+
+import (
+	"strings"
+
+	"github.com/fluxcd/pkg/ssa"
+	"sigs.k8s.io/cli-utils/pkg/object"
+)
+
+const (
+	newLine      = "\n"
+	emptySpace   = "    "
+	middleItem   = "├── "
+	continueItem = "│   "
+	lastItem     = "└── "
+)
+
+type (
+	objMetadataTree struct {
+		objMetadata object.ObjMetadata
+		items       []ObjMetadataTree
+	}
+
+	ObjMetadataTree interface {
+		Add(objMetadata object.ObjMetadata) ObjMetadataTree
+		AddTree(tree ObjMetadataTree)
+		Items() []ObjMetadataTree
+		Text() string
+		Print() string
+	}
+
+	printer struct {
+	}
+
+	Printer interface {
+		Print(ObjMetadataTree) string
+	}
+)
+
+func New(objMetadata object.ObjMetadata) ObjMetadataTree {
+	return &objMetadataTree{
+		objMetadata: objMetadata,
+		items:       []ObjMetadataTree{},
+	}
+}
+
+func (t *objMetadataTree) Add(objMetadata object.ObjMetadata) ObjMetadataTree {
+	n := New(objMetadata)
+	t.items = append(t.items, n)
+	return n
+}
+
+func (t *objMetadataTree) AddTree(tree ObjMetadataTree) {
+	t.items = append(t.items, tree)
+}
+
+func (t *objMetadataTree) Text() string {
+	return ssa.FmtObjMetadata(t.objMetadata)
+}
+
+func (t *objMetadataTree) Items() []ObjMetadataTree {
+	return t.items
+}
+
+func (t *objMetadataTree) Print() string {
+	return newPrinter().Print(t)
+}
+
+func newPrinter() Printer {
+	return &printer{}
+}
+
+func (p *printer) Print(t ObjMetadataTree) string {
+	return t.Text() + newLine + p.printItems(t.Items(), []bool{})
+}
+
+func (p *printer) printText(text string, spaces []bool, last bool) string {
+	var result string
+	for _, space := range spaces {
+		if space {
+			result += emptySpace
+		} else {
+			result += continueItem
+		}
+	}
+
+	indicator := middleItem
+	if last {
+		indicator = lastItem
+	}
+
+	var out string
+	lines := strings.Split(text, "\n")
+	for i := range lines {
+		text := lines[i]
+		if i == 0 {
+			out += result + indicator + text + newLine
+			continue
+		}
+		if last {
+			indicator = emptySpace
+		} else {
+			indicator = continueItem
+		}
+		out += result + indicator + text + newLine
+	}
+
+	return out
+}
+
+func (p *printer) printItems(t []ObjMetadataTree, spaces []bool) string {
+	var result string
+	for i, f := range t {
+		last := i == len(t)-1
+		result += p.printText(f.Text(), spaces, last)
+		if len(f.Items()) > 0 {
+			spacesChild := append(spaces, last)
+			result += p.printItems(f.Items(), spacesChild)
+		}
+	}
+	return result
+}
-- 
GitLab