From e8594afcd6c29c9a573464892d0c645c1e2cdf65 Mon Sep 17 00:00:00 2001 From: Akash Kumar Date: Sun, 19 Jul 2026 20:55:14 +0530 Subject: [PATCH] Guard against Kustomization tree cycles Track Kustomizations in the active traversal path so cyclic inventories stop before issuing repeated API requests. Add a golden regression test for a two-object inventory cycle. Assisted-by: codex/gpt-5 Signed-off-by: Akash Kumar --- cmd/flux/testdata/tree/kustomizations.yaml | 35 ++++++++++++++++++++++ cmd/flux/testdata/tree/tree-cycle.golden | 3 ++ cmd/flux/tree_kustomization.go | 32 ++++++++++++++------ cmd/flux/tree_kustomization_test.go | 14 +++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 cmd/flux/testdata/tree/tree-cycle.golden diff --git a/cmd/flux/testdata/tree/kustomizations.yaml b/cmd/flux/testdata/tree/kustomizations.yaml index 98a619e911..2772d9318c 100644 --- a/cmd/flux/testdata/tree/kustomizations.yaml +++ b/cmd/flux/testdata/tree/kustomizations.yaml @@ -86,3 +86,38 @@ status: status: "True" type: Ready --- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cycle-a + namespace: {{ .fluxns }} +spec: + interval: 5m + path: ./cycle-a + prune: true + sourceRef: + kind: GitRepository + name: flux-system +status: + inventory: + entries: + - id: {{ .fluxns }}_cycle-b_kustomize.toolkit.fluxcd.io_Kustomization + v: v1 +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: cycle-b + namespace: {{ .fluxns }} +spec: + interval: 5m + path: ./cycle-b + prune: true + sourceRef: + kind: GitRepository + name: flux-system +status: + inventory: + entries: + - id: {{ .fluxns }}_cycle-a_kustomize.toolkit.fluxcd.io_Kustomization + v: v1 diff --git a/cmd/flux/testdata/tree/tree-cycle.golden b/cmd/flux/testdata/tree/tree-cycle.golden new file mode 100644 index 0000000000..2edc122aee --- /dev/null +++ b/cmd/flux/testdata/tree/tree-cycle.golden @@ -0,0 +1,3 @@ +Kustomization/{{ .fluxns }}/cycle-a +└── Kustomization/{{ .fluxns }}/cycle-b + diff --git a/cmd/flux/tree_kustomization.go b/cmd/flux/tree_kustomization.go index f41e34287d..ece5b27f24 100644 --- a/cmd/flux/tree_kustomization.go +++ b/cmd/flux/tree_kustomization.go @@ -101,7 +101,7 @@ func treeKsCmdRun(cmd *cobra.Command, args []string) error { GroupKind: schema.GroupKind{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}, }) - err = treeKustomization(ctx, kTree, k, kubeClient, treeKsArgs.compact) + err = treeKustomization(ctx, kTree, k, kubeClient, treeKsArgs.compact, map[client.ObjectKey]struct{}{}) if err != nil { return err } @@ -126,7 +126,11 @@ func treeKsCmdRun(cmd *cobra.Command, args []string) error { return nil } -func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kustomizev1.Kustomization, kubeClient client.Client, compact bool) error { +func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kustomizev1.Kustomization, kubeClient client.Client, compact bool, ancestors map[client.ObjectKey]struct{}) error { + itemKey := client.ObjectKeyFromObject(item) + ancestors[itemKey] = struct{}{} + defer delete(ancestors, itemKey) + if item.Status.Inventory == nil || len(item.Status.Inventory.Entries) == 0 { return nil } @@ -143,13 +147,26 @@ func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kus continue } - if objMetadata.GroupKind.Group == kustomizev1.GroupVersion.Group && - objMetadata.GroupKind.Kind == kustomizev1.KustomizationKind && + isKustomization := objMetadata.GroupKind.Group == kustomizev1.GroupVersion.Group && + objMetadata.GroupKind.Kind == kustomizev1.KustomizationKind + + if isKustomization && objMetadata.Namespace == item.Namespace && objMetadata.Name == item.Name { continue } + canRecurse := isKustomization && item.Spec.KubeConfig == nil + if canRecurse { + key := client.ObjectKey{ + Namespace: objMetadata.Namespace, + Name: objMetadata.Name, + } + if _, ok := ancestors[key]; ok { + continue + } + } + ks := tree.Add(objMetadata) if objMetadata.GroupKind.Group == helmv2.GroupVersion.Group && @@ -171,10 +188,7 @@ func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kus } } - if objMetadata.GroupKind.Group == kustomizev1.GroupVersion.Group && - objMetadata.GroupKind.Kind == kustomizev1.KustomizationKind && - // skip kustomization if it targets a remote clusters - item.Spec.KubeConfig == nil { + if canRecurse { k := &kustomizev1.Kustomization{} err = kubeClient.Get(ctx, client.ObjectKey{ Namespace: objMetadata.Namespace, @@ -183,7 +197,7 @@ func treeKustomization(ctx context.Context, tree tree.ObjMetadataTree, item *kus if err != nil { return fmt.Errorf("failed to find object: %w", err) } - err := treeKustomization(ctx, ks, k, kubeClient, compact) + err := treeKustomization(ctx, ks, k, kubeClient, compact, ancestors) if err != nil { return err } diff --git a/cmd/flux/tree_kustomization_test.go b/cmd/flux/tree_kustomization_test.go index 045b869bfd..a2312cb660 100644 --- a/cmd/flux/tree_kustomization_test.go +++ b/cmd/flux/tree_kustomization_test.go @@ -48,9 +48,23 @@ func TestTree(t *testing.T) { "testdata/tree/kustomizations.yaml", "testdata/tree/tree-empty.golden", }, + { + "tree kustomization cycle", + "tree kustomization cycle-a --timeout=1s", + "testdata/tree/kustomizations.yaml", + "testdata/tree/tree-cycle.golden", + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { + previousTimeout := rootArgs.timeout + timeoutFlag := rootCmd.PersistentFlags().Lookup("timeout") + previousTimeoutChanged := timeoutFlag.Changed + t.Cleanup(func() { + rootArgs.timeout = previousTimeout + timeoutFlag.Changed = previousTimeoutChanged + }) + tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), }