You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Part of #983. Unblocks the BOM-per-leaf prereq in #966.
Summary
The only way to obtain a component's resolved Helm values today is through pkg/bundler.DefaultBundler.Make, which writes files to disk. The merge logic itself — base values from ValuesFile deep-merged with ComponentRef.Overrides — lives in mergeValues in pkg/recipe/adapter.go, which is package-private.
Expose it as a public method on *recipe.RecipeResult so consumers (the BOM tool, in-process Go callers, future tooling) can obtain effective values without going through the bundler or duplicating the merge algorithm.
Motivation
Two concrete consumers are blocked:
chore(recipes): make resolved recipes the single source of truth for chart versions #966's BOM rewrite. Prereq 1 requires "resolve each through pkg/recipe, and for every componentRef, run helm template with the same effective values that bundling uses." There is no public API for that today. The BOM tool either reimplements values merging (drift hazard — two divergent implementations of the same merge order) or duplicates bundler internals (coupling hazard). Both are bad.
In-process Go consumers. Any tool that wants to inspect "what values would actually be deployed for component X under recipe Y" — diff tools, dry-run validators, GitOps generators — needs this primitive. Without it, every such consumer reinvents the merge.
The merge logic is correct and well-tested via the bundler. Promoting it to the public surface is a low-risk additive change; no new behavior, just a new entry point.
Proposed change
Add two public methods on *RecipeResult (in pkg/recipe/metadata.go or a new pkg/recipe/values.go):
// GetValuesForComponent returns the effective Helm values for the named// component using the process-global DataProvider. Equivalent to the// values that the bundler would write to values.yaml for this component.//// Merge order matches the bundler:// 1. base values from ComponentRef.ValuesFile (if set)// 2. overlay values (if the overlay system contributed any for this component)// 3. ComponentRef.Overrides (highest precedence)//// Returns ErrCodeNotFound if name is not in r.ComponentRefs.// Returns ErrCodeInternal on values-file read or parse failure.func (r*RecipeResult) GetValuesForComponent(namestring) (map[string]any, error)
And a manifest-content helper at package level for components that carry ManifestFiles:
// GetManifestContent reads a manifest path (as recorded in// ComponentRef.ManifestFiles or PreManifestFiles) from the process-// global DataProvider and returns its contents.//// Returns ErrCodeNotFound when the file is absent.// Returns ErrCodeInternal on read failure.funcGetManifestContent(pathstring) ([]byte, error)
Semantics
Both helpers MUST use the same merge / read logic the bundler currently uses internally. Refactor — do not reimplement. The bundler should be updated to call these new public helpers so we don't have two code paths to maintain.
The returned map from GetValuesForComponent is owned by the caller and safe to mutate. Internal cached state must be deep-copied (see the related cache-corruption fix in the parent epic).
A missing ValuesFile is not an error — it produces a values map derived purely from Overrides. An empty result is map[string]any{}, not nil, so callers can distinguish "no values" from a marshaling edge case.
The package-private mergeValues stays private. The public contract is the GetValuesForComponent method, not the algorithm.
Tests
Add table-driven coverage in pkg/recipe/metadata_test.go (or values_test.go) for:
Component not in ComponentRefs → ErrCodeNotFound.
Component with no ValuesFile, no Overrides → empty map.
Component with ValuesFile only → values from file.
Component with Overrides only → values from overrides.
Component with both → correct deep merge (precedence: file < overrides).
Component with nested maps in both → deep merge into the same key.
Missing values file path → ErrCodeInternal wrapping the read error.
Add a bundler test asserting that bundler.DefaultBundler.Make and RecipeResult.GetValuesForComponent produce identical values for the same component — pins the equivalence and prevents future drift.
Documentation
Update docs/contributor/data.md to document the new public helpers and their merge order.
Update the pkg/recipe package godoc.
Acceptance
RecipeResult.GetValuesForComponent and recipe.GetManifestContent are public, documented, and tested.
pkg/bundler uses the new helpers internally — single code path for the merge.
The bundler's existing behavior is byte-identical before and after the refactor (verify by running KWOK recipe tests against main and the PR branch, comparing emitted values.yaml for every component in every leaf).
make qualify clean.
docs/contributor/data.md updated.
Out of scope
Adding a WithProvider variant of either helper. That's a follow-up that pairs with [Epic]: Library-readiness hardening for in-process Go consumers #983's per-Builder DataProvider isolation work — once isolation lands, both helpers gain an optional dp parameter so in-process consumers with multiple recipe sources can bound the read to a specific provider. Until isolation lands, both helpers correctly fall back to the package-global GetDataProvider().
Changing the merge order. Pure refactor of where the algorithm lives, not what it does.
Streaming or memory-bounded variants. The bundler already reads files into memory; this just exposes the same path.
Dependencies
Soft dependency on the per-Builder DataProvider isolation work (separate child issue). This issue can land first and a follow-up will add the WithProvider variants once isolation exists.
Part of #983. Unblocks the BOM-per-leaf prereq in #966.
Summary
The only way to obtain a component's resolved Helm values today is through
pkg/bundler.DefaultBundler.Make, which writes files to disk. The merge logic itself — base values fromValuesFiledeep-merged withComponentRef.Overrides— lives inmergeValuesinpkg/recipe/adapter.go, which is package-private.Expose it as a public method on
*recipe.RecipeResultso consumers (the BOM tool, in-process Go callers, future tooling) can obtain effective values without going through the bundler or duplicating the merge algorithm.Motivation
Two concrete consumers are blocked:
chore(recipes): make resolved recipes the single source of truth for chart versions #966's BOM rewrite. Prereq 1 requires "resolve each through
pkg/recipe, and for everycomponentRef, runhelm templatewith the same effective values that bundling uses." There is no public API for that today. The BOM tool either reimplements values merging (drift hazard — two divergent implementations of the same merge order) or duplicates bundler internals (coupling hazard). Both are bad.In-process Go consumers. Any tool that wants to inspect "what values would actually be deployed for component X under recipe Y" — diff tools, dry-run validators, GitOps generators — needs this primitive. Without it, every such consumer reinvents the merge.
The merge logic is correct and well-tested via the bundler. Promoting it to the public surface is a low-risk additive change; no new behavior, just a new entry point.
Proposed change
Add two public methods on
*RecipeResult(inpkg/recipe/metadata.goor a newpkg/recipe/values.go):And a manifest-content helper at package level for components that carry
ManifestFiles:Semantics
GetValuesForComponentis owned by the caller and safe to mutate. Internal cached state must be deep-copied (see the related cache-corruption fix in the parent epic).ValuesFileis not an error — it produces a values map derived purely fromOverrides. An empty result ismap[string]any{}, not nil, so callers can distinguish "no values" from a marshaling edge case.mergeValuesstays private. The public contract is theGetValuesForComponentmethod, not the algorithm.Tests
pkg/recipe/metadata_test.go(orvalues_test.go) for:ComponentRefs→ErrCodeNotFound.ValuesFile, noOverrides→ empty map.ValuesFileonly → values from file.Overridesonly → values from overrides.ErrCodeInternalwrapping the read error.bundler.DefaultBundler.MakeandRecipeResult.GetValuesForComponentproduce identical values for the same component — pins the equivalence and prevents future drift.Documentation
docs/contributor/data.mdto document the new public helpers and their merge order.pkg/recipepackage godoc.Acceptance
RecipeResult.GetValuesForComponentandrecipe.GetManifestContentare public, documented, and tested.pkg/bundleruses the new helpers internally — single code path for the merge.mainand the PR branch, comparing emittedvalues.yamlfor every component in every leaf).make qualifyclean.docs/contributor/data.mdupdated.Out of scope
WithProvidervariant of either helper. That's a follow-up that pairs with [Epic]: Library-readiness hardening for in-process Go consumers #983's per-Builder DataProvider isolation work — once isolation lands, both helpers gain an optionaldpparameter so in-process consumers with multiple recipe sources can bound the read to a specific provider. Until isolation lands, both helpers correctly fall back to the package-globalGetDataProvider().Dependencies
DataProviderisolation work (separate child issue). This issue can land first and a follow-up will add theWithProvidervariants once isolation exists.