Skip to content

Commit c0e47fd

Browse files
committed
Add .status.inventory to track managed objects
This adds the `.status.inventory` field to HelmRelease, similar to Kustomization, to expose managed Kubernetes objects. The inventory includes: - Objects from the release manifest (with namespace complement) - CRDs from the chart's crds/ directory Helm hooks are excluded as they are ephemeral resources deleted after execution. Signed-off-by: cappyzawa <cappyzawa@gmail.com>
1 parent e537e73 commit c0e47fd

File tree

10 files changed

+702
-0
lines changed

10 files changed

+702
-0
lines changed

.github/workflows/e2e.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,23 @@ jobs:
6060
run: |
6161
kubectl -n helm-system apply -f config/testdata/podinfo
6262
kubectl -n helm-system wait helmreleases/podinfo --for=condition=ready --timeout=4m
63+
64+
# Inventory tracking enables drift detection and garbage collection.
65+
# Ensure it captures managed objects from the Helm release.
66+
INVENTORY=$(kubectl -n helm-system get helmrelease/podinfo -o jsonpath='{.status.inventory.entries}')
67+
INVENTORY_COUNT=$(echo "$INVENTORY" | jq 'length')
68+
if [ "$INVENTORY_COUNT" -lt 1 ]; then
69+
echo "Expected inventory entries, got $INVENTORY_COUNT"
70+
exit 1
71+
fi
72+
# Deployment is a primary workload resource; its presence confirms
73+
# that the inventory correctly tracks resources from the rendered manifests.
74+
if ! echo "$INVENTORY" | jq -e '.[] | select(.id | contains("_Deployment"))' > /dev/null; then
75+
echo "Expected Deployment in inventory"
76+
echo "Inventory: $INVENTORY"
77+
exit 1
78+
fi
79+
6380
kubectl -n helm-system wait helmreleases/podinfo-git --for=condition=ready --timeout=4m
6481
kubectl -n helm-system wait helmreleases/podinfo-oci --for=condition=ready --timeout=4m
6582
kubectl -n helm-system delete -f config/testdata/podinfo

api/v2/helmrelease_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,11 @@ type HelmReleaseStatus struct {
11821182
// +optional
11831183
History Snapshots `json:"history,omitempty"`
11841184

1185+
// Inventory contains the list of Kubernetes resource object references
1186+
// that have been applied for this release.
1187+
// +optional
1188+
Inventory *ResourceInventory `json:"inventory,omitempty"`
1189+
11851190
// LastAttemptedReleaseAction is the last release action performed for this
11861191
// HelmRelease. It is used to determine the active retry or remediation
11871192
// strategy.

api/v2/inventory_types.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
Copyright 2025 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v2
18+
19+
// ResourceInventory contains a list of Kubernetes resource object references
20+
// that have been applied by a HelmRelease.
21+
type ResourceInventory struct {
22+
// Entries of Kubernetes resource object references.
23+
Entries []ResourceRef `json:"entries"`
24+
}
25+
26+
// ResourceRef contains the information necessary to locate a resource within a cluster.
27+
type ResourceRef struct {
28+
// ID is the string representation of the Kubernetes resource object's metadata,
29+
// in the format '<namespace>_<name>_<group>_<kind>'.
30+
ID string `json:"id"`
31+
32+
// Version is the API version of the Kubernetes resource object's kind.
33+
Version string `json:"v"`
34+
}

api/v2/zz_generated.deepcopy.go

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/helm.toolkit.fluxcd.io_helmreleases.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,34 @@ spec:
12481248
state. It is reset after a successful reconciliation.
12491249
format: int64
12501250
type: integer
1251+
inventory:
1252+
description: |-
1253+
Inventory contains the list of Kubernetes resource object references
1254+
that have been applied for this release.
1255+
properties:
1256+
entries:
1257+
description: Entries of Kubernetes resource object references.
1258+
items:
1259+
description: ResourceRef contains the information necessary
1260+
to locate a resource within a cluster.
1261+
properties:
1262+
id:
1263+
description: |-
1264+
ID is the string representation of the Kubernetes resource object's metadata,
1265+
in the format '<namespace>_<name>_<group>_<kind>'.
1266+
type: string
1267+
v:
1268+
description: Version is the API version of the Kubernetes
1269+
resource object's kind.
1270+
type: string
1271+
required:
1272+
- id
1273+
- v
1274+
type: object
1275+
type: array
1276+
required:
1277+
- entries
1278+
type: object
12511279
lastAttemptedConfigDigest:
12521280
description: |-
12531281
LastAttemptedConfigDigest is the digest for the config (better known as

docs/api/v2/helm.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1669,6 +1669,21 @@ up to the last successfully completed release.</p>
16691669
</tr>
16701670
<tr>
16711671
<td>
1672+
<code>inventory</code><br>
1673+
<em>
1674+
<a href="#helm.toolkit.fluxcd.io/v2.ResourceInventory">
1675+
ResourceInventory
1676+
</a>
1677+
</em>
1678+
</td>
1679+
<td>
1680+
<em>(Optional)</em>
1681+
<p>Inventory contains the list of Kubernetes resource object references
1682+
that have been applied for this release.</p>
1683+
</td>
1684+
</tr>
1685+
<tr>
1686+
<td>
16721687
<code>lastAttemptedReleaseAction</code><br>
16731688
<em>
16741689
<a href="#helm.toolkit.fluxcd.io/v2.ReleaseAction">
@@ -2342,6 +2357,85 @@ UpgradeRemediation.</p>
23422357
</p>
23432358
<p>RemediationStrategy returns the strategy to use to remediate a failed install
23442359
or upgrade.</p>
2360+
<h3 id="helm.toolkit.fluxcd.io/v2.ResourceInventory">ResourceInventory
2361+
</h3>
2362+
<p>
2363+
(<em>Appears on:</em>
2364+
<a href="#helm.toolkit.fluxcd.io/v2.HelmReleaseStatus">HelmReleaseStatus</a>)
2365+
</p>
2366+
<p>ResourceInventory contains a list of Kubernetes resource object references
2367+
that have been applied by a HelmRelease.</p>
2368+
<div class="md-typeset__scrollwrap">
2369+
<div class="md-typeset__table">
2370+
<table>
2371+
<thead>
2372+
<tr>
2373+
<th>Field</th>
2374+
<th>Description</th>
2375+
</tr>
2376+
</thead>
2377+
<tbody>
2378+
<tr>
2379+
<td>
2380+
<code>entries</code><br>
2381+
<em>
2382+
<a href="#helm.toolkit.fluxcd.io/v2.ResourceRef">
2383+
[]ResourceRef
2384+
</a>
2385+
</em>
2386+
</td>
2387+
<td>
2388+
<p>Entries of Kubernetes resource object references.</p>
2389+
</td>
2390+
</tr>
2391+
</tbody>
2392+
</table>
2393+
</div>
2394+
</div>
2395+
<h3 id="helm.toolkit.fluxcd.io/v2.ResourceRef">ResourceRef
2396+
</h3>
2397+
<p>
2398+
(<em>Appears on:</em>
2399+
<a href="#helm.toolkit.fluxcd.io/v2.ResourceInventory">ResourceInventory</a>)
2400+
</p>
2401+
<p>ResourceRef contains the information necessary to locate a resource within a cluster.</p>
2402+
<div class="md-typeset__scrollwrap">
2403+
<div class="md-typeset__table">
2404+
<table>
2405+
<thead>
2406+
<tr>
2407+
<th>Field</th>
2408+
<th>Description</th>
2409+
</tr>
2410+
</thead>
2411+
<tbody>
2412+
<tr>
2413+
<td>
2414+
<code>id</code><br>
2415+
<em>
2416+
string
2417+
</em>
2418+
</td>
2419+
<td>
2420+
<p>ID is the string representation of the Kubernetes resource object&rsquo;s metadata,
2421+
in the format &lsquo;<namespace><em><name></em><group>_<kind>&rsquo;.</p>
2422+
</td>
2423+
</tr>
2424+
<tr>
2425+
<td>
2426+
<code>v</code><br>
2427+
<em>
2428+
string
2429+
</em>
2430+
</td>
2431+
<td>
2432+
<p>Version is the API version of the Kubernetes resource object&rsquo;s kind.</p>
2433+
</td>
2434+
</tr>
2435+
</tbody>
2436+
</table>
2437+
</div>
2438+
</div>
23452439
<h3 id="helm.toolkit.fluxcd.io/v2.Retry">Retry
23462440
</h3>
23472441
<p>Retry defines a consistent interface for retry strategies from

internal/inventory/inventory.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
Copyright 2025 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package inventory
18+
19+
import (
20+
"bytes"
21+
"fmt"
22+
"slices"
23+
"strings"
24+
25+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
28+
29+
"github.com/fluxcd/cli-utils/pkg/object"
30+
ssautil "github.com/fluxcd/pkg/ssa/utils"
31+
32+
v2 "github.com/fluxcd/helm-controller/api/v2"
33+
helmchart "helm.sh/helm/v4/pkg/chart/v2"
34+
helmrelease "helm.sh/helm/v4/pkg/release/v1"
35+
)
36+
37+
// New returns a new ResourceInventory with an empty Entries slice.
38+
func New() *v2.ResourceInventory {
39+
return &v2.ResourceInventory{
40+
Entries: []v2.ResourceRef{},
41+
}
42+
}
43+
44+
// AddManifest parses the manifest, complements namespaces, and adds the objects to the inventory.
45+
func AddManifest(inv *v2.ResourceInventory, manifest string, releaseNamespace string, c client.Client) error {
46+
objects, err := parseManifest(manifest)
47+
if err != nil {
48+
return fmt.Errorf("failed to parse manifest: %w", err)
49+
}
50+
51+
// NOTE: Helm hooks are ephemeral resources that are deleted after execution,
52+
// so they should not be tracked in the inventory.
53+
objects = slices.DeleteFunc(objects, func(obj *unstructured.Unstructured) bool {
54+
annotations := obj.GetAnnotations()
55+
if annotations == nil {
56+
return false
57+
}
58+
_, isHook := annotations[helmrelease.HookAnnotation]
59+
return isHook
60+
})
61+
62+
if err := setNamespaces(objects, releaseNamespace, c); err != nil {
63+
return fmt.Errorf("failed to set namespaces: %w", err)
64+
}
65+
66+
for _, obj := range objects {
67+
objMeta := object.UnstructuredToObjMetadata(obj)
68+
inv.Entries = append(inv.Entries, v2.ResourceRef{
69+
ID: objMeta.String(),
70+
Version: obj.GroupVersionKind().Version,
71+
})
72+
}
73+
return nil
74+
}
75+
76+
func parseManifest(manifest string) ([]*unstructured.Unstructured, error) {
77+
return ssautil.ReadObjects(strings.NewReader(manifest))
78+
}
79+
80+
// setNamespaces complements namespace for namespaced objects that don't have one set,
81+
// following the pattern established in internal/action/diff.go.
82+
// This is necessary because Helm manifests don't include namespace for namespaced resources.
83+
func setNamespaces(objects []*unstructured.Unstructured, releaseNamespace string, c client.Client) error {
84+
isNamespacedGVK := map[string]bool{}
85+
86+
for _, obj := range objects {
87+
if obj.GetNamespace() != "" {
88+
continue
89+
}
90+
91+
objGVK := obj.GetObjectKind().GroupVersionKind().String()
92+
if _, ok := isNamespacedGVK[objGVK]; !ok {
93+
namespaced, err := apiutil.IsObjectNamespaced(obj, c.Scheme(), c.RESTMapper())
94+
if err != nil {
95+
return fmt.Errorf("failed to determine if %s is namespace scoped: %w",
96+
obj.GetObjectKind().GroupVersionKind().Kind, err)
97+
}
98+
isNamespacedGVK[objGVK] = namespaced
99+
}
100+
101+
if isNamespacedGVK[objGVK] {
102+
obj.SetNamespace(releaseNamespace)
103+
}
104+
}
105+
106+
return nil
107+
}
108+
109+
// AddCRDs adds CRDs from the chart's crds/ directory to the inventory.
110+
// CRDs are cluster-scoped, so no namespace complement is needed.
111+
func AddCRDs(inv *v2.ResourceInventory, chart *helmchart.Chart) error {
112+
for _, crd := range chart.CRDObjects() {
113+
objects, err := ssautil.ReadObjects(bytes.NewBuffer(crd.File.Data))
114+
if err != nil {
115+
return fmt.Errorf("failed to parse CRD %s: %w", crd.Name, err)
116+
}
117+
118+
for _, obj := range objects {
119+
objMeta := object.UnstructuredToObjMetadata(obj)
120+
inv.Entries = append(inv.Entries, v2.ResourceRef{
121+
ID: objMeta.String(),
122+
Version: obj.GroupVersionKind().Version,
123+
})
124+
}
125+
}
126+
return nil
127+
}

0 commit comments

Comments
 (0)