Skip to content

feat(recipe): expose effective-values and manifest helpers on RecipeResult #985

Description

@mchmarny

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:

  1. 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.

  2. 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(name string) (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.
func GetManifestContent(path string) ([]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 ComponentRefsErrCodeNotFound.
    • 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

Metadata

Metadata

Assignees

No one assigned

    Fields

    No fields configured for Enhancement.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions