Skip to content

feat(helm): support optional: prefix on individual valueFiles entries (#27698)#27699

Open
thoro wants to merge 1 commit intoargoproj:masterfrom
thoro:feat/helm-optional-value-files-prefix
Open

feat(helm): support optional: prefix on individual valueFiles entries (#27698)#27699
thoro wants to merge 1 commit intoargoproj:masterfrom
thoro:feat/helm-optional-value-files-prefix

Conversation

@thoro
Copy link
Copy Markdown

@thoro thoro commented May 5, 2026

Disclaimer: This PR was created and drafted together with Claude Opus 4.7

Closes #27698.

What this PR does

Adds an optional: prefix that can be applied to any single entry in
source.helm.valueFiles. A prefixed entry behaves as if
ignoreMissingValueFiles were set, but only for that one entry — sibling
entries without the prefix remain strict.

source:
  helm:
    valueFiles:
      - shared/defaults.yaml                          # required
      - optional:shared/defaults.region-eu-west.yaml  # optional
      - service/defaults.yaml                         # required
      - optional:service/defaults.region-eu-west.yaml
      - service/defaults.production.yaml              # required
      - optional:service/defaults.production-canary.yaml

Behavior:

  • Missing required entry: error (unchanged).
  • Missing optional: entry: silently skipped.
  • optional: on a glob: zero-match is silently skipped instead of raising
    GlobNoMatchError.
  • optional: composes with $ref sources, env substitution, glob expansion,
    and remote URLs.
  • ignoreMissingValueFiles: true keeps working unchanged. When set, bare
    entries continue to be silently skipped on miss; optional: entries are
    also silently skipped (|| semantics).
  • Order is preserved — Helm precedence is positional and each override stays
    in its declared position next to its base.

Why a string prefix instead of a structured field

valueFiles is []string today. A typed {path, optional} element would
better match Kubernetes API style (and matches how configMapKeyRef.optional
etc. are expressed) but is an API break — every consumer of valueFiles,
including ApplicationSet templates and the CLI, would need to handle both
forms. A reserved string prefix lands the feature with no schema change, no
CRD bump, and no valueFiles v2 field, while preserving the positional
ordering that a separate optionalValueFiles list would lose.

The literal token optional: becomes reserved at the start of valueFiles
entries. Linux filesystems do allow : in filenames, but it is rare in
practice (clashes with host:path rsync/scp conventions and PATH
separators). If a real file is genuinely named optional:something.yaml,
the entry can be escaped by prefixing with ./ — only entries that start
with the literal token are treated as markers, so ./optional:legacy.yaml
is parsed as a path. The docs call this out explicitly.

Why the word optional

Kubernetes already uses optional: true for the same semantic on
configMapKeyRef, secretKeyRef, envFrom, and volumes.*. Anyone reading
an ArgoCD manifest will recognize the word. Alternatives considered:

  • ? prefix — collides visually with the doublestar single-character glob.
  • - prefix — collides with the YAML list bullet.
  • opt: — abbreviation; no precedent.

Implementation

Self-contained to reposerver/repository/repository.go:

  1. New helper splitOptionalPrefix(raw string) (string, bool) strips the
    optional: token and reports whether it was present. Uses
    strings.CutPrefix to match the existing parsing style in the same file
    (e.g. alias: handling at line 1158).

  2. The helper is called at every site that interprets a valueFiles string,
    so the marker composes with every existing feature without further parser
    updates:

    • getReferencedSourceName — central ref-source parser; covers
      getReferencedSources and getReferencedSource automatically.
    • The three inline strings.HasPrefix(valueFile, "$") ref-preflight sites
      in resolveReferencedSources, runManifestGenAsync, and
      UpdateRevisionForPaths.
    • The two loops inside getResolvedValueFiles (the pre-pass that builds
      explicitPaths, and the main resolution loop).
  3. In getResolvedValueFiles, the per-entry optional flag is OR'd into
    the two existing skip branches:

    // glob with zero matches
    if ignoreMissingValueFiles || optional { continue }
    
    // os.IsNotExist on a non-glob path
    if ignoreMissingValueFiles || optional { continue }

That is the entire functional diff. No CRD bump, no API version bump, no
ApplicationSet template changes, no CLI changes. Existing manifests behave
identically.

Tests

In reposerver/repository/repository_test.go:

  • Test_splitOptionalPrefix — unit-tests the helper across bare paths,
    prefixed paths, prefixed $ref, prefixed glob, empty rest, and
    case-sensitivity.
  • Test_getResolvedValueFiles_optional — integration-level cases:
    • optional: on a missing plain path → skipped.
    • optional: on an existing plain path → included.
    • optional: on an existing $ref source → resolved through the ref.
    • optional: on a missing $ref source path → skipped.
    • optional: on a glob with matches → behaves like a normal glob.
    • optional: on a glob with zero matches → skipped without error.
    • Required missing sibling next to optional missing → required is still
      passed through to helm (helm errors later, current contract).
    • Required missing glob sibling next to optional missing glob → required
      still raises GlobNoMatchError.
    • Optional next to required-and-present → order preserved.
    • ignoreMissingValueFiles=true paired with optional: entries →
      behavior unchanged for bare entries; optional: continues to work.

All new and existing valueFile-related tests pass under -race.

Documentation

docs/user-guide/helm.md gains a "Marking a single file as optional"
section right next to the existing ignoreMissingValueFiles description,
with a synthetic apps-of-apps example, a note that the marker composes with
ref sources / globs / remote URLs, and a callout that optional: is a
reserved prefix.

Backwards compatibility

  • valueFiles remains []string — no CRD, OpenAPI, or API version change.
  • Manifests without optional: entries are unaffected.
  • ignoreMissingValueFiles keeps current semantics.
  • argocd app set --values <path> and ApplicationSet template-produced
    strings can carry optional: transparently — no CLI or template code
    changes needed.

Feature status

Proposed as Stable per the feature-status rubric:

  • Backwards compatible by construction — bare entries behave exactly as
    before; only entries that explicitly start with optional: opt in.
  • No CRD, API version, schema, or flag change; nothing to deprecate or
    evolve out of.
  • Surface area is one reserved prefix on one field — very little that
    could change incompatibly later.
  • Covered by unit tests; existing valueFile/glob tests still pass.

Happy to demote to Beta (with a [BETA] marker on the docs section)
if maintainers prefer the conservative path. Alpha would mean wiring
a feature flag to gate prefix recognition; I'd rather not add that
unless explicitly requested, since a silently no-op optional: on
default config is its own footgun.

Related issues

Per-entry optionality complements the existing all-or-nothing
ignoreMissingValueFiles flag and preserves positional ordering.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Rosenstein <thomas.rosenstein@avafin.com>
@thoro thoro requested review from a team as code owners May 5, 2026 12:47
@bunnyshell
Copy link
Copy Markdown

bunnyshell Bot commented May 5, 2026

✅ Preview Environment deployed on Bunnyshell

Component Endpoints
argocd https://argocd-nhuohb.bunnyenv.com/
argocd-ttyd https://argocd-web-cli-nhuohb.bunnyenv.com/

See: Environment Details | Pipeline Logs

Available commands (reply to this comment):

  • 🔴 /bns:stop to stop the environment
  • 🚀 /bns:deploy to redeploy the environment
  • /bns:delete to remove the environment

@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.85%. Comparing base (14b9d68) to head (45f2c51).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #27699   +/-   ##
=======================================
  Coverage   63.84%   63.85%           
=======================================
  Files         418      418           
  Lines       57269    57279   +10     
=======================================
+ Hits        36566    36574    +8     
  Misses      17290    17290           
- Partials     3413     3415    +2     

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: per-file optional marker for Helm valueFiles

1 participant