From 80ef184b60874c5f5fbe8fce35f8b6206920ac7b Mon Sep 17 00:00:00 2001 From: Stefan Prodan 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 --- 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 +}