Repository navigation
fix(helm): handle charts that render resources inside list kinds - #5344
Conversation
✅ Deploy Preview for zarf-docs canceled.
|
AustinAbro321
left a comment
There was a problem hiding this comment.
Thank you for the PR! A few comments, but overall it's a reasonable change
Codecov Report❌ Patch coverage is
... and 4 files with indirect coverage changes 🚀 New features to boost your workflow:
|
|
9973550 adds a fix to another issue I encountered, further described in the PR description under heading spec.template is not always a pod template. |
AustinAbro321
left a comment
There was a problem hiding this comment.
A few more comments, appreciate the cycles
| // the items it holds rather than the list itself | ||
| if err := eachResource(rawData, func(obj *unstructured.Unstructured) error { | ||
| r.recordConnectString(ctx, obj) | ||
| r.trackNamespace(obj) |
There was a problem hiding this comment.
We end up with a problem here where namespaces in a NamespaceList get into the final manifest and therefore removed on package removal.
I think a cleaner solution would be to have a function like this called at the top of EditHelmResources. This way the rest of our logic doesn't have to worry about resource lists
// flattenHelmResources expands list documents into their leaf resources before Zarf assigns ownership
func flattenHelmResources(resources []releaseutil.Manifest) ([]releaseutil.Manifest, error) {
flattened := make([]releaseutil.Manifest, 0, len(resources))
for _, resource := range resources {
_, rawData, err := processManifestContent(resource.Content, nil)
if err != nil {
return nil, err
}
if len(rawData.Object) == 0 || !rawData.IsList() {
flattened = append(flattened, resource)
continue
}
err = flattenListResource(rawData, func(obj *unstructured.Unstructured) error {
content, err := yaml.Marshal(obj.Object)
if err != nil {
return fmt.Errorf("failed to marshal list item: %w", err)
}
leaf := resource
leaf.Content = string(content)
flattened = append(flattened, leaf)
return nil
})
if err != nil {
return nil, err
}
}
return flattened, nil
}
func flattenListResource(obj *unstructured.Unstructured, addResource func(*unstructured.Unstructured) error) error {
if !obj.IsList() {
return addResource(obj)
}
return obj.EachListItem(func(item runtime.Object) error {
listItem, ok := item.(*unstructured.Unstructured)
if !ok {
return fmt.Errorf("unexpected list item type %T", item)
}
return flattenListResource(listItem, addResource)
})
}There was a problem hiding this comment.
Thanks, yea that's a bunch better. Taken as suggested.
One side effect though. Helm tracks which file a document came from with a postrenderer.helm.sh/postrender-filename annotation it stamps before calling post-renderer. And on a list that lands on the wrapper rather than the items, so flattening drops it. This is helm get manifest on the e2e fixture:
# Source: generated-by-postrender-1.yaml item of a ConfigMapList
# Source: generated-by-postrender-2.yaml item of a ConfigMapList
# Source: list-kinds/templates/generic-list.yaml item of a generic List
I think nothing downstream reads it, and the annotation never reaches the cluster. It is only that the generic list keeps its name, since annotateAndMerge (check PR description why this is) expanded that one before stamping, so two things that should look the same no longer do. If you want, I can copy the annotation onto each item while flattening if you would rather.
There was a problem hiding this comment.
Good callout. From my evaluation it's fine if the annotation is dropped. Looks easy enough to add, but I wouldn't want to have to update this if a future version of Helm to drop or change the annotation
A chart is free to render its resources inside a list kind, and the post
renderer read every document as a single resource. A list is a wrapper
around what its items hold rather than a resource itself, so reading it as
one broke four things.
The package label went onto the list's own metadata, which is a ListMeta
and has no labels field, so the API server rejected the deploy:
ValidationError(ConfigMapList.metadata): unknown field "labels" in
io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta
A Service carrying the zarf-connect label was never registered, since the
kind of the document is ConfigMapList or List, never Service. A namespace
named only by items was never tracked, since GetNamespace on the wrapper
reads a ListMeta and returns "", so zarf never created it and helm had
nowhere to apply the item. A Namespace held by a list stayed owned by
helm, so it never got the zarf managed labels and helm deleted it on
uninstall along with whatever lived in it.
Expand list documents into the resources they hold before any of that
runs, so the rest of the post renderer only ever sees one resource per
manifest and none of it has to know lists exist. Helm flattens a list into
its items before applying it anyway, so the same objects reach the cluster
either way.
Detection is IsList, which is what helm's kube client uses through
meta.IsListType when it flattens, so zarf and helm agree on which
documents hold more than one resource.
Signed-off-by: Igor de Beijer <71566757+idebeijer@users.noreply.github.com>
Deploy a chart that renders a ConfigMapList, a generic List, a NamespaceList and an item pointing at a namespace nothing else creates, then select the results by their package label. It fails if helm rejects the deploy, if items land unlabeled, if a label the chart set on an item is lost, if the namespace an item names is missing, or if removing the package takes a namespace held by a list with it. Signed-off-by: Igor de Beijer <71566757+idebeijer@users.noreply.github.com>
9973550 to
b6bfb71
Compare
EachListItem reports an item that is not an object with the type it wanted
rather than the one it got
items member is not an object: map[string]interface {}
which names neither the document nor the offending value. Wrap it with the
kind of the list so there is something to go looking for.
Also correct the comment on the flattened item. It claimed the item keeps
the name of the file the list came from, which is not true downstream:
helm's filename annotation sits on the wrapper, so a flattened item arrives
back at helm unannotated and is filed under a generated name.
The e2e read a namespace's name to check it outlived the package, which a
terminating namespace still answers with. Read the phase instead.
Signed-off-by: Igor de Beijer <71566757+idebeijer@users.noreply.github.com>
| // field, so a label written there is rejected by the API server, and everything zarf keys off the | ||
| // kind of a document misses what the list holds. Helm flattens a list into its items before | ||
| // applying it anyway, so the items are what ends up in the cluster either way. | ||
| func flattenHelmResources(resources []releaseutil.Manifest) ([]releaseutil.Manifest, error) { |
There was a problem hiding this comment.
Looking at the two loops (in order) we have flattenHelmResources -> editHelmResources and each call processManifestContent. So unconditionally this now adds a full decode/encode round trip and the shape of each is pretty similar - negligible on small charts but a code smell.
There was a problem hiding this comment.
Good point, I'll see if I can add optimize this later.
brandtkeller
left a comment
There was a problem hiding this comment.
lgtm - I think we can iterate on optimizing as needed but this solves a real problem as is.
Thanks, I hadn't taken the time to optimize yet. I'll see if I still can in a later PR, because I did run a benchmark after your feedback and it wasn't great. |
…f-dev#5344) Signed-off-by: Igor de Beijer <71566757+idebeijer@users.noreply.github.com>
Description
The post-renderer made two assumptions about every rendered document: that it holds a single resource, and that anything at
spec.templateis a pod template. Neither always holds, and each one fails the deploy of a chart that is valid for Helm andkubectlalike.It made the manifest invalid. The zarf package label went onto the document's
metadata, but a list's metadata is aListMeta, which has nolabelsfield, so the API server rejected the deploy before anything in the list was created:A
Serviceinside a list was never registered as a connect string, since the document's kind isConfigMapListorList, neverService.A namespace named only by list items was never tracked, since
GetNamespace()on the wrapper reads aListMetaand returns"". Zarf creates namespaces before Helm applies, so a chart whose list items point at a namespace that doesn't exist yet failed outright:A
Namespaceinside a list stayed owned by Helm, so it never gotAdoptZarfManagedLabelsand Helm deleted it on uninstall along with whatever else lived in it.Rather than teach each of those to walk into items,
flattenHelmResourcesreplaces a list document with the resources it held before any of them run, so the rest of the post-renderer only ever sees one resource per manifest and none of it changes. Helm flattens a list into its items before applying it anyway, so the objects that reach the cluster are the same either way.Detection is
obj.IsList(), the same check Helm's kube client uses viameta.IsListType()when it flattens, so Zarf and Helm agree on which documents hold more than one resource.Which lists actually reach the post-renderer
Only typed ones. Before running a post-renderer, Helm merges the rendered files with
annotateAndMerge, which pipes every document throughkio.ParseAll(https://github.com/helm/helm/blob/v4.2.0/pkg/action/action.go#L206). That expands a generickind: Listinto its items, so a chart using one never arrives here as a list. Typed list kinds (ConfigMapList,SecretList, etc) are left intact and are the ones that broke. The generic case is covered anyway, since that expansion is a kustomize dependency inside Helm rather than a documented contract.Related Issue
Fixes #5343
Checklist before merging