From 3e80c5809e29f27ea479ab9ac1225a551d7236d2 Mon Sep 17 00:00:00 2001
From: Max Jonas Werner <max@coppersoft.com>
Date: Wed, 14 May 2025 16:11:56 +0200
Subject: [PATCH] Fix `flux trace` for HRs from `OCIRepository`s

Before:
```
$ flux -n default trace pod default-podinfo-585856f49c-4jl4m
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x40 pc=0x10618da70]

goroutine 1 [running]:
main.traceHelm({0x106dd7b28, 0x14000201490}, {0x12f34c0d8, 0x14000783100}, {{0x1400071e130?, 0x1061e7795?}, {0x1400071e109?, 0x1000d9c84?}}, 0x140006a6030)
	/home/runner/work/flux2/flux2/cmd/flux/trace.go:404 +0x2f0
main.traceObject({0x106dd7b28, 0x14000201490}, {0x12f34c0d8, 0x14000783100}, 0x140006a6030)
	/home/runner/work/flux2/flux2/cmd/flux/trace.go:134 +0x11c
main.traceObjects({0x106dd7b28, 0x14000201490}, {0x12f34c0d8, 0x14000783100}, {0x140006a6040, 0x1, 0x0?})
	/home/runner/work/flux2/flux2/cmd/flux/trace.go:112 +0x74
main.traceCmdRun(0x14000592800?, {0x140003aea80, 0x2, 0x4})
	/home/runner/work/flux2/flux2/cmd/flux/trace.go:107 +0x180
github.com/spf13/cobra.(*Command).execute(0x108341980, {0x140003aea40, 0x4, 0x4})
	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.8.1/command.go:985 +0x834
github.com/spf13/cobra.(*Command).ExecuteC(0x108329280)
	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.8.1/command.go:1117 +0x344
github.com/spf13/cobra.(*Command).Execute(...)
	/home/runner/go/pkg/mod/github.com/spf13/cobra@v1.8.1/command.go:1041
main.main()
	/home/runner/work/flux2/flux2/cmd/flux/main.go:189 +0x78
```

After:

```
 $ ~/dev/flux/flux2/bin/flux -n default trace pod default-podinfo-585856f49c-4jl4m

Object:         Pod/default-podinfo-585856f49c-4jl4m
Namespace:      default
Status:         Managed by Flux
---
HelmRelease:    podinfo
Namespace:      flux-system
Target:         default
Revision:       6.8.0+2360bdf32ddc
Status:         Last reconciled at 2025-05-14 16:10:37 +0200 CEST
Message:        Helm install succeeded for release default/default-podinfo.v1 with chart podinfo@6.8.0+2360bdf32ddc
---
OCIRepository:   podinfo
Namespace:       flux-system
URL:             oci://ghcr.io/stefanprodan/charts/podinfo
Tag:             6.8.0
Revision:        6.8.0@sha256:2360bdf32ddc50c05f8e128118173343b0a012a338daf145b16e0da9c80081a4
Status:          Last reconciled at 2025-05-14 16:09:17 +0200 CEST
Message:         stored artifact for digest '6.8.0@sha256:2360bdf32ddc50c05f8e128118173343b0a012a338daf145b16e0da9c80081a4'
```

Signed-off-by: Max Jonas Werner <max@coppersoft.com>
---
 .../trace/deployment-hr-ocirepo.golden        |  18 +++
 .../testdata/trace/deployment-hr-ocirepo.yaml |  86 +++++++++++++
 cmd/flux/trace.go                             | 116 +++++++++++++-----
 cmd/flux/trace_test.go                        |  12 ++
 4 files changed, 204 insertions(+), 28 deletions(-)
 create mode 100644 cmd/flux/testdata/trace/deployment-hr-ocirepo.golden
 create mode 100644 cmd/flux/testdata/trace/deployment-hr-ocirepo.yaml

diff --git a/cmd/flux/testdata/trace/deployment-hr-ocirepo.golden b/cmd/flux/testdata/trace/deployment-hr-ocirepo.golden
new file mode 100644
index 00000000..34b03f37
--- /dev/null
+++ b/cmd/flux/testdata/trace/deployment-hr-ocirepo.golden
@@ -0,0 +1,18 @@
+
+Object:         deployment/podinfo
+Namespace:      {{ .ns }}
+Status:         Managed by Flux
+---
+HelmRelease:    podinfo
+Namespace:      {{ .ns }}
+Revision:       6.3.5
+Status:         Last reconciled at {{ .helmReleaseLastReconcile }}
+Message:        Release reconciliation succeeded
+---
+OCIRepository:   podinfo-charts
+Namespace:       {{ .fluxns }}
+URL:             oci://ghcr.io/stefanprodan/charts/podinfo
+Tag:             6.8.0
+Revision:        sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3
+Status:          Last reconciled at {{ .ociRepositoryLastReconcile }}
+Message:         stored artifact for digest 'sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3'
diff --git a/cmd/flux/testdata/trace/deployment-hr-ocirepo.yaml b/cmd/flux/testdata/trace/deployment-hr-ocirepo.yaml
new file mode 100644
index 00000000..29f0c1b5
--- /dev/null
+++ b/cmd/flux/testdata/trace/deployment-hr-ocirepo.yaml
@@ -0,0 +1,86 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: {{ .fluxns }}
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: {{ .ns }}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    app.kubernetes.io/name: podinfo
+    app.kubernetes.io/managed-by: Helm
+    helm.toolkit.fluxcd.io/name: podinfo
+    helm.toolkit.fluxcd.io/namespace: {{ .ns }}
+  name: podinfo
+  namespace: {{ .ns }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: podinfo
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: podinfo
+    spec:
+      containers:
+      - name: hello
+        command: [ "echo hello world" ]
+        image: busybox
+---
+apiVersion: helm.toolkit.fluxcd.io/v2
+kind: HelmRelease
+metadata:
+  name: podinfo
+  namespace: {{ .ns }}
+spec:
+  chartRef:
+    kind: OCIRepository
+    name: podinfo-charts
+    namespace: {{ .fluxns }}
+  interval: 5m
+status:
+  conditions:
+  - lastTransitionTime: "2021-07-16T15:42:20Z"
+    message: Release reconciliation succeeded
+    reason: ReconciliationSucceeded
+    status: "True"
+    type: Ready
+  lastAttemptedRevision: 6.3.5
+---
+apiVersion: source.toolkit.fluxcd.io/v1beta2
+kind: OCIRepository
+metadata:
+  labels:
+    kustomize.toolkit.fluxcd.io/name: flux-system
+    kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
+  name: podinfo-charts
+  namespace: {{ .fluxns }}
+spec:
+  interval: 10m0s
+  provider: generic
+  ref:
+    tag: 6.8.0
+  timeout: 60s
+  url: oci://ghcr.io/stefanprodan/charts/podinfo
+status:
+  artifact:
+    lastUpdateTime: "2022-08-10T10:07:59Z"
+    metadata:
+      org.opencontainers.image.revision: 6.1.6@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0
+      org.opencontainers.image.source: https://github.com/stefanprodan/podinfo.git
+    path: "example"
+    revision: sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3
+    url: "example"
+  conditions:
+  - lastTransitionTime: "2021-07-20T00:48:16Z"
+    message: "stored artifact for digest 'sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3'"
+    reason: Succeed
+    status: "True"
+    type: Ready
diff --git a/cmd/flux/trace.go b/cmd/flux/trace.go
index a62e9a52..9946daae 100644
--- a/cmd/flux/trace.go
+++ b/cmd/flux/trace.go
@@ -401,38 +401,65 @@ func traceHelm(ctx context.Context, kubeClient client.Client, hrName types.Names
 
 	var hrGitRepository *sourcev1.GitRepository
 	var hrGitRepositoryReady *metav1.Condition
-	if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.GitRepositoryKind {
-		hrGitRepository = &sourcev1.GitRepository{}
-		sourceNamespace := hr.Namespace
-		if hr.Spec.Chart.Spec.SourceRef.Namespace != "" {
-			sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace
-		}
-		err = kubeClient.Get(ctx, types.NamespacedName{
-			Namespace: sourceNamespace,
-			Name:      hr.Spec.Chart.Spec.SourceRef.Name,
-		}, hrGitRepository)
-		if err != nil {
-			return "", fmt.Errorf("failed to find GitRepository: %w", err)
-		}
-		hrGitRepositoryReady = meta.FindStatusCondition(hrGitRepository.Status.Conditions, fluxmeta.ReadyCondition)
-	}
-
 	var hrHelmRepository *sourcev1.HelmRepository
 	var hrHelmRepositoryReady *metav1.Condition
-	if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.HelmRepositoryKind {
-		hrHelmRepository = &sourcev1.HelmRepository{}
-		sourceNamespace := hr.Namespace
-		if hr.Spec.Chart.Spec.SourceRef.Namespace != "" {
-			sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace
+	var hrOCIRepository *sourcev1b2.OCIRepository
+	var hrOCIRepositoryReady *metav1.Condition
+	if hr.Spec.Chart == nil {
+		if hr.Spec.ChartRef != nil {
+			switch hr.Spec.ChartRef.Kind {
+			case sourcev1b2.OCIRepositoryKind:
+				hrOCIRepository = &sourcev1b2.OCIRepository{}
+				sourceNamespace := hr.Namespace
+				if hr.Spec.ChartRef.Namespace != "" {
+					sourceNamespace = hr.Spec.ChartRef.Namespace
+				}
+				err = kubeClient.Get(ctx, types.NamespacedName{
+					Namespace: sourceNamespace,
+					Name:      hr.Spec.ChartRef.Name,
+				}, hrOCIRepository)
+				if err != nil {
+					return "", fmt.Errorf("failed to find OCIRepository: %w", err)
+				}
+				hrOCIRepositoryReady = meta.FindStatusCondition(hrOCIRepository.Status.Conditions, fluxmeta.ReadyCondition)
+			}
+			kubeClient.Get(ctx, types.NamespacedName{
+				Namespace: hr.Spec.ChartRef.Namespace,
+				Name:      hr.Spec.ChartRef.Name,
+			}, hrOCIRepository)
 		}
-		err = kubeClient.Get(ctx, types.NamespacedName{
-			Namespace: sourceNamespace,
-			Name:      hr.Spec.Chart.Spec.SourceRef.Name,
-		}, hrHelmRepository)
-		if err != nil {
-			return "", fmt.Errorf("failed to find HelmRepository: %w", err)
+	} else {
+		if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.GitRepositoryKind {
+			hrGitRepository = &sourcev1.GitRepository{}
+			sourceNamespace := hr.Namespace
+			if hr.Spec.Chart.Spec.SourceRef.Namespace != "" {
+				sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace
+			}
+			err = kubeClient.Get(ctx, types.NamespacedName{
+				Namespace: sourceNamespace,
+				Name:      hr.Spec.Chart.Spec.SourceRef.Name,
+			}, hrGitRepository)
+			if err != nil {
+				return "", fmt.Errorf("failed to find GitRepository: %w", err)
+			}
+			hrGitRepositoryReady = meta.FindStatusCondition(hrGitRepository.Status.Conditions, fluxmeta.ReadyCondition)
+		}
+
+		if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.HelmRepositoryKind {
+			hrHelmRepository = &sourcev1.HelmRepository{}
+			sourceNamespace := hr.Namespace
+			if hr.Spec.Chart.Spec.SourceRef.Namespace != "" {
+				sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace
+			}
+			err = kubeClient.Get(ctx, types.NamespacedName{
+				Namespace: sourceNamespace,
+				Name:      hr.Spec.Chart.Spec.SourceRef.Name,
+			}, hrHelmRepository)
+			if err != nil {
+				return "", fmt.Errorf("failed to find HelmRepository: %w", err)
+			}
+			hrHelmRepositoryReady = meta.FindStatusCondition(hrHelmRepository.Status.Conditions, fluxmeta.ReadyCondition)
 		}
-		hrHelmRepositoryReady = meta.FindStatusCondition(hrHelmRepository.Status.Conditions, fluxmeta.ReadyCondition)
 	}
 
 	var traceTmpl = `
@@ -515,6 +542,34 @@ Message:       {{.GitRepositoryReady.Message}}
 Status:        Unknown
 {{- end }}
 {{- end }}
+{{- if .OCIRepository }}
+---
+OCIRepository:   {{.OCIRepository.Name}}
+Namespace:       {{.OCIRepository.Namespace}}
+URL:             {{.OCIRepository.Spec.URL}}
+{{- if .OCIRepository.Spec.Reference }}
+{{- if .OCIRepository.Spec.Reference.Tag }}
+Tag:             {{.OCIRepository.Spec.Reference.Tag}}
+{{- else if .OCIRepository.Spec.Reference.SemVer }}
+Tag:             {{.OCIRepository.Spec.Reference.SemVer}}
+{{- else if .OCIRepository.Spec.Reference.Digest }}
+Digest:          {{.OCIRepository.Spec.Reference.Digest}}
+{{- end }}
+{{- end }}
+{{- if .OCIRepository.Status.Artifact }}
+Revision:        {{.OCIRepository.Status.Artifact.Revision}}
+{{- end }}
+{{- if .OCIRepositoryReady }}
+{{- if eq .OCIRepositoryReady.Status "False" }}
+Status:          Last reconciliation failed at {{.OCIRepositoryReady.LastTransitionTime}}
+{{- else }}
+Status:          Last reconciled at {{.OCIRepositoryReady.LastTransitionTime}}
+{{- end }}
+Message:         {{.OCIRepositoryReady.Message}}
+{{- else }}
+Status:          Unknown
+{{- end }}
+{{- end }}
 `
 
 	traceResult := struct {
@@ -528,6 +583,9 @@ Status:        Unknown
 		GitRepositoryReady  *metav1.Condition
 		HelmRepository      *sourcev1.HelmRepository
 		HelmRepositoryReady *metav1.Condition
+		OCIRepository       *sourcev1b2.OCIRepository
+		OCIRepositoryReady  *metav1.Condition
+		Annotations         map[string]string
 	}{
 		ObjectName:          obj.GetKind() + "/" + obj.GetName(),
 		ObjectNamespace:     obj.GetNamespace(),
@@ -539,6 +597,8 @@ Status:        Unknown
 		GitRepositoryReady:  hrGitRepositoryReady,
 		HelmRepository:      hrHelmRepository,
 		HelmRepositoryReady: hrHelmRepositoryReady,
+		OCIRepository:       hrOCIRepository,
+		OCIRepositoryReady:  hrOCIRepositoryReady,
 	}
 
 	t, err := template.New("tmpl").Parse(traceTmpl)
diff --git a/cmd/flux/trace_test.go b/cmd/flux/trace_test.go
index 0f11f6da..c3fca3b5 100644
--- a/cmd/flux/trace_test.go
+++ b/cmd/flux/trace_test.go
@@ -85,6 +85,18 @@ func TestTrace(t *testing.T) {
 				"ociRepositoryLastReconcile": toLocalTime(t, "2021-07-20T00:48:16Z"),
 			},
 		},
+		{
+			"Deployment from HelmRelease from OCI registry",
+			"trace podinfo --kind deployment --api-version=apps/v1",
+			"testdata/trace/deployment-hr-ocirepo.yaml",
+			"testdata/trace/deployment-hr-ocirepo.golden",
+			map[string]string{
+				"ns":                         allocateNamespace("podinfo"),
+				"fluxns":                     allocateNamespace("flux-system"),
+				"helmReleaseLastReconcile":   toLocalTime(t, "2021-07-16T15:42:20Z"),
+				"ociRepositoryLastReconcile": toLocalTime(t, "2021-07-20T00:48:16Z"),
+			},
+		},
 	}
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-- 
GitLab