Skip to content

feat(controller): resolve Promotion.spec.target into promotion context - #6791

Open
jsbroks wants to merge 1 commit into
mainfrom
justin/promotion-target-context
Open

feat(controller): resolve Promotion.spec.target into promotion context#6791
jsbroks wants to merge 1 commit into
mainfrom
justin/promotion-target-context

Conversation

@jsbroks

@jsbroks jsbroks commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Description

The promotion engine has been able to expose a Target's params and labels to step expressions as the top-level target variable since #6608BuildCtxMap populates env["target"] from Context.Target, and promotion.TargetContext has existed alongside it. But nothing ever set Context.Target, so ${{ target.params.* }} never resolved and the plumbing was unreachable.

This PR closes that gap: the Promotions controller now loads the Target named by a Promotion's spec.target and builds a TargetContext from it.

Without this, every Promotion fanned out across a Stage's Targets would run byte-identical steps — per-destination parameterization is the entire point of Targets, so this is the piece that turns the fan-out into a feature.

Scope: the Promotion → Target read path only. No Stage-side selector resolution, no Target.status.ownedBy, no webhooks, no PromotionSet involvement. Those follow in later PRs.

What changed

  • pkg/api/target.goGetTarget, mirroring the existing GetStage/GetWarehouse helpers.
  • pkg/promotion/promotion.goNewTargetContext(*kargoapi.Target) decodes spec.params from map[string]apiextensionsv1.JSON into the plain values expressions operate on, and clones labels so the context does not alias the fetched object. Adds the WithTarget ContextOption.
  • pkg/controller/promotions/promotions.goresolveTargetContext loads the Target and returns nil for Promotions that name none. Wired into promote() at the NewContext call.

Promotions that name no Target promote to the Stage itself and carry no target context, so a reference to target.* fails exactly as it did before Targets existed. Behavior for every existing Promotion is unchanged.

Notes for reviewers

Why conversion is a separate constructor rather than folded into WithTarget. The JSON decode can fail, and ContextOption has no way to report an error. Keeping NewTargetContext separate lets the controller surface a clear error on the Promotion instead of silently dropping params.

Governance is deliberately not validated here. The controller fails a Promotion whose spec.target names a nonexistent Target, but will currently promote to a Target the referenced Stage does not govern. Checking that requires evaluating the Stage's targetSelectors, which belongs in the admission webhook rather than duplicated in the controller — otherwise two places own the same rule. No privilege escalation is involved (users already have get/list/watch on Targets in their own Project), but it is a correctness gap to close when the webhook lands.

One behavior worth knowing, now pinned by a test. Numeric params reach a step as int, not the float64 they are decoded to, because step config is round-tripped through YAML in StepEvaluator.Config. This is consistent with how every other config value behaves, so nothing changed — but it is now asserted rather than assumed.

Testing

  • TestGetTarget — not-found, client error, and success.
  • TestNewTargetContext — nil Target, scalar/nested/absent params, malformed params, label copying, and that decoded params survive DeepCopy (which relies on runtime.DeepCopyJSON and would panic on anything that is not a plain JSON type).
  • TestWithTarget — nil leaves the Context target-less; non-nil is set.
  • Test_reconciler_resolveTargetContext — including that a Promotion naming no Target performs no read at all.
  • Test_reconciler_promote_targetContext — asserts the resolved context actually reaches the engine, which is the real integration point.
  • TestStepEvaluator_Config_target — end-to-end through the real evaluator: ${{ target.params.branch }}, ${{ target.params.ingress.host }}, and ${{ target.labels.region }} resolve in step config, and a target.* reference still errors when there is no Target.

make lint-go is clean. Affected packages pass under -race.

pkg/promotion/runner/builtin has one failure locally (Test_kustomizeBuilder_run/successful_build_with_HelmChartInflationGenerator), confirmed via git stash to fail identically on a clean tree — a local environment issue, unrelated to this change.

Checklist

Eligibility

  • Linked to an existing issue with no blocking labels (kind/proposal, needs discussion, needs research, maintainer only, area/security, size/large, size/x-large, size/xx-large).
  • Changes documentation only.
  • Changes ten lines or fewer.

Quality

  • Adds or updates corresponding tests.
  • Adds or updates corresponding documentation.

AI Use Disclosure

This PR was written:

  • By a human without AI assistance.
  • By a human with AI assistance. A human has reviewed every line prior to opening the PR.
  • By an AI with human supervision. A human has reviewed every line prior to opening the PR.
  • Entirely by an AI. No human has reviewed this prior to opening the PR.

Sign-Off

All commits:

  • Are signed off by their author (git commit -s) (required)
  • Are cryptographically signed (git commit -S) (encouraged)

The promotion engine has been able to expose a Target's params and labels
to step expressions as the top-level `target` variable, but nothing ever
populated Context.Target, so `target.*` never resolved.

Load the Target named by a Promotion's spec.target and build a
TargetContext from it, decoding spec.params from JSON into the plain
values expressions operate on. Promotions that name no Target promote to
the Stage itself and carry no target context, so any reference to
`target.*` fails exactly as it did before Targets existed.

This is what allows one Stage's promotion process to behave differently
per destination rather than running byte-identical steps everywhere.

Signed-off-by: Justin Brooks <justin.brooks@akuity.io>
@kargo-governance-bot kargo-governance-bot Bot added needs/area Issue or PR needs to be labeled to indicate what parts of the code base are affected needs/kind Issue or PR needs to be labeled to clarify its nature needs/priority Priority has not yet been determined; a good signal that maintainers aren't fully committed labels Aug 11, 2026
@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for docs-kargo-io ready!

Name Link
🔨 Latest commit 32fcab6
🔍 Latest deploy log https://app.netlify.com/projects/docs-kargo-io/deploys/6a7b5654551eee000821ec92
😎 Deploy Preview https://deploy-preview-6791.docs.kargo.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@jsbroks
jsbroks marked this pull request as ready for review August 11, 2026 17:09
@jsbroks
jsbroks requested a review from a team as a code owner August 11, 2026 17:09
@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.55172% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 30.96%. Comparing base (a8df2f9) to head (32fcab6).

Files with missing lines Patch % Lines
pkg/controller/promotions/promotions.go 92.85% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6791      +/-   ##
==========================================
+ Coverage   30.84%   30.96%   +0.11%     
==========================================
  Files         826      827       +1     
  Lines       80666    80724      +58     
==========================================
+ Hits        24885    24997     +112     
+ Misses      54327    54262      -65     
- Partials     1454     1465      +11     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs/area Issue or PR needs to be labeled to indicate what parts of the code base are affected needs/kind Issue or PR needs to be labeled to clarify its nature needs/priority Priority has not yet been determined; a good signal that maintainers aren't fully committed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants