diff --git a/hips/hip-0028.md b/hips/hip-0028.md new file mode 100644 index 00000000..3cd14cd0 --- /dev/null +++ b/hips/hip-0028.md @@ -0,0 +1,287 @@ +--- +hip: 9999 +title: "Expose Release History During Template Rendering" +authors: ["Andrew Shoell "] +created: "2025-11-12" +type: "feature" +status: "draft" +--- + +## Abstract + +This HIP proposes exposing release history metadata during template rendering. Currently, Helm templates have access to `.Chart` for the chart being installed but no equivalent access to deployed release history. This forces chart authors to use complex workarounds like post-renderers, pre-upgrade hooks, or manual values conventions to implement version-aware upgrade logic. + +The proposal introduces `.Release.History` (array of historical releases) available in template contexts, populated during `helm upgrade` and `helm rollback` operations when the `--release-history-max` flag is provided. The flag controls how many historical releases to retrieve (default: 0), requiring explicit opt-in. Chart authors can check `len .Release.History` to determine if sufficient historical data is available and use `.Release.Revision` (or compare `len .Release.History` to expected minimum) to detect upgrade scenarios requiring migration logic. + +## Motivation + +### Current Limitations + +Helm provides comprehensive chart metadata through `.Chart` but offers no native way to access deployed release metadata during template evaluation. Chart developers must resort to problematic workarounds: + +**Post-Renderers:** External tools that query the cluster, parse manifests, and make version-aware modifications. This moves upgrade logic outside the chart, requires additional tooling, and breaks Helm's self-contained design. + +**Pre-Upgrade Hooks:** Store version metadata in ConfigMaps via hooks, creating ordering dependencies and potential failure points. + +**Manual Values:** Require users to specify previous versions in values files—error-prone and defeats Helm's release tracking. + +### Real-World Impact + +This limitation prevents or complicates legitimate use cases: + +- **Breaking Changes:** No clean migration path for renamed resources or changed structures +- **Conditional Resources:** Cannot create migration Jobs based on version deltas +- **Smart Defaults:** Cannot distinguish fresh installs from upgrades for intelligent defaults +- **Advanced Deployments:** Blue-green and similar strategies require external orchestration + +Post-rendering solutions violate Helm's design philosophy that template rendering should be deterministic and self-contained. Making deployed chart metadata available at template time keeps upgrade logic in the chart itself, maintaining Helm's portability, testability, and transparency. + +## Rationale + +### Naming: `.Release.History` + +`.Release.History` extends the existing `.Release` built-in object with a history array, making it immediately intuitive and discoverable for Helm users. This follows the established pattern of `.Release.Name`, `.Release.Namespace`, `.Release.Revision`, etc., and feels like a natural part of Helm's API. + +The history array is ordered in reverse chronological order (index 0 is most recent deployed release). This provides ergonomic access to the most recent release while enabling multi-version migrations when needed. + +Alternatives considered and rejected: + +- `.PreviousChart` - Ambiguous during rollbacks +- `.DeployedChart`/`.DeployedCharts` - Less discoverable, doesn't extend existing objects +- `.InstalledChart` - Confusing with current installation +- `.CurrentChart` - Ambiguous which is "current" +- `.Release.Deployed.Chart` - Unnecessarily nested + +### Always Available as Template Object + +`.Release.History` (empty array or populated) is always present to ensure consistent template behavior, prevent undefined variable errors, and enable testing with `helm template`. + +### Populated Only During Upgrades/Rollbacks + +`.Release.History` contains release metadata only during `helm upgrade` and `helm rollback` when deployed releases exist and `--release-history-max` is greater than 0. During rollback, the history reflects releases deployed before the rollback target. It's empty for: + +- `helm install` - No deployed release +- `helm template` / dry-runs - No cluster context +- When `--release-history-max 0` is used (default) + +### Filtered Release Data + +This proposal exposes a filtered subset of release data to balance utility with performance and security: + +**Included by default:** + +- Chart metadata (Name, Version, AppVersion, and other Chart.yaml fields) +- Release metadata (Name, Namespace, Revision, Status) +- Timestamps (FirstDeployed, LastDeployed) + +**Excluded by default (opt-in via flag):** + +- Values (may contain secrets; use `--include-history-values` to include) +- Manifests (can be very large; future consideration) +- Templates (can be very large; no clear use case - templates are static per chart version) +- Hooks (implementation detail) + +The conservative default excludes potentially sensitive or large data. Users who need historical values for complex migration scenarios can opt-in explicitly with `--include-history-values`, accepting the security and performance tradeoffs. See Security Implications section for detailed rationale. + +**Future consideration:** Historical manifests could be made available via `--include-history-manifests` if demand exists, though manifests can be quite large and increase memory/performance overhead significantly. + +### Max Control Flag + +The `--release-history-max` flag controls how many historical releases to retrieve (default: 0, requiring explicit opt-in). This conservative default protects users from accidental performance impact. Setting `--release-history-max 0` explicitly disables the feature (though this is already the default). Higher max values may impact performance and should only be used for specific multi-version migration scenarios. + +### Design Decisions + +- **Different Chart Names:** Still populates `.Release.History` even if chart names differ—templates can detect and handle this +- **Helm's Record:** Reflects Helm's stored release record, not actual cluster state (use `lookup()` for that) +- **Dry-Run/Template:** Always empty array to maintain cluster-agnostic, deterministic behavior +- **Opt-In by Default:** Default max of 0 requires explicit user choice, preventing accidental performance impact + +## Specification + +### New Template Objects + +**`.Release.History`**: Array of release objects in reverse chronological order (most recent first). Empty array if no deployed releases exist or `--release-history-max 0` is used (default). + +**Note:** Use `.Release.Revision` to detect if this is an upgrade (revision > 1) and `len .Release.History` to check how much historical data is available. + +**Release Object Structure:** Each release object contains: + +```go +type HistoricalRelease struct { + Name string // Release name + Namespace string // Release namespace + Revision int // Revision number + Status string // Release status (deployed, superseded, failed, etc.) + Chart Chart // Chart metadata (same structure as .Chart) + FirstDeployed time.Time // When this release was first deployed + LastDeployed time.Time // When this release was last deployed +} +``` + +**Usage Examples:** + +```yaml +# Check if upgrading from a version that needs migration +{{- if gt (len .Release.History) 0 }} +{{- $lastRelease := index .Release.History 0 }} +{{- if and (semverCompare ">=2.0.0" .Chart.Version) (semverCompare "<2.0.0" $lastRelease.Chart.Version) }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "mychart.fullname" . }}-migration + annotations: + "helm.sh/hook": pre-upgrade +spec: + template: + spec: + containers: + - name: migrate + image: myapp/migrator:{{ .Chart.AppVersion }} + command: ["migrate", "v1-to-v2"] +{{- end }} +{{- end }} + +# Require minimum depth for safe upgrades +{{- if lt .Release.HistoryDepth 3 }} + {{- fail "This chart requires --release-history-depth=3 for safe upgrades from v2.x" }} +{{- end }} + +# Multi-version migration: handle complex upgrade paths +{{- range .Release.History }} +{{- if and (eq .Status "deployed") (semverCompare "<1.5.0" .Chart.Version) }} +# Run migration for successfully deployed versions before 1.5.0 +{{- end }} +{{- end }} + +# Check if last deployment was successful +{{- if gt (len .Release.History) 0 }} +{{- $lastRelease := index .Release.History 0 }} +{{- if ne $lastRelease.Status "deployed" }} + {{- fail (printf "Previous release was %s - manual intervention required" $lastRelease.Status) }} +{{- end }} +{{- end }} +``` + +### Command-Line Flag + +```bash +# Default: no history retrieved (max 0) +helm upgrade myrelease mychart + +# Retrieve latest deployed release +helm upgrade myrelease mychart --release-history-max 1 + +# Retrieve last 3 releases +helm upgrade myrelease mychart --release-history-max 3 + +# Explicitly disable (same as default) +helm upgrade myrelease mychart --release-history-max 0 +``` + +### Behavior Matrix + +The following table shows what values are available in template context for different operations: + +| Operation | `.Release.History` | `.Release.Revision` | +| ------------------------------------------------- | ------------------------------------- | ------------------- | +| `helm install` | `[]` | 1 | +| `helm upgrade` (first) | `[]` (default, no flag) | 2 | +| `helm upgrade --release-history-max 1` (first) | Populated with 1 release | 2 | +| `helm upgrade --release-history-max N` | Up to N releases | varies | +| `helm rollback --release-history-max N` | Populated with releases before target | varies | +| `helm template` / dry-runs | `[]` | 1 | + +**Note:** Use `.Release.Revision` to distinguish installs (revision=1) from upgrades (revision>1). + +## Backwards Compatibility + +Fully backwards compatible. The `.Release.History` field is purely additive—existing charts work unchanged. Go templates handle empty arrays safely; the recommended `{{ if gt (len .Release.History) 0 }}` pattern works in all scenarios. Default max of 0 means existing behavior is unchanged unless users explicitly opt in. + +## Security Implications + +**Not Exposed:** Previous values (may contain secrets) or previous manifests (may contain sensitive data). Only filtered release metadata is exposed (chart metadata, release status, timestamps, revision numbers). + +**Considerations:** Chart authors should not store sensitive data in Chart.yaml. The default `--release-history-depth 0` provides opt-out by default. Higher depth values increase data exposure; use the minimum required. Release status and metadata are already stored in cluster secrets by Helm, so this doesn't expose data that isn't already persisted. + +## How to Teach This + +### Documentation Additions + +1. **Template Objects Reference:** Add `.Release.History` to built-in objects documentation with availability details and usage patterns with `.Release.Revision` +2. **Upgrade Guide:** "Implementing Version-Aware Upgrades" covering empty array checks, version comparisons, status checking, and best practices +3. **Migration Examples:** Show replacement of post-renderers and pre-upgrade hooks, including use of opt-in flag for values when needed, with practical patterns (check last release, check last successful) +4. **Performance Note:** Document that `--release-history-max` should be kept minimal; opt-in by default protects users +5. **Chart Linting:** Update `helm lint` to warn on `.Release.History` usage without empty array checks, and suggest using `.Release.Revision` for upgrade detection +6. **Security Guide:** Document the opt-in flag `--include-history-values` with clear warnings about security implications + +### Key Example Pattern + +```yaml +# Pattern 1: Defensive check before accessing history +{{- if gt (len .Release.History) 0 }} +{{- $lastRelease := index .Release.History 0 }} +{{- if semverCompare "<3.0.0" $lastRelease.Chart.Version }} +# Handle breaking change from versions < 3.0.0 +{{- end }} +{{- end }} + +# Pattern 2: Check only last successful deployment +{{- $lastSuccessful := dict }} +{{- range .Release.History }} + {{- if eq .Status "deployed" }} + {{- $lastSuccessful = . }} + {{- break }} + {{- end }} +{{- end }} +{{- if $lastSuccessful }} + # Use $lastSuccessful for migration logic +{{- end }} + +# Pattern 3: Require history for upgrades (not installs) +{{- if gt .Release.Revision 1 }} + {{- if eq (len .Release.History) 0 }} + {{- fail "Upgrades require --release-history-max=1 for continuity checks" }} + {{- end }} +{{- end }} + +# Pattern 4: Check for sufficient history depth (less common) +{{- if and (gt .Release.Revision 1) (lt (len .Release.History) 2) }} + {{- fail "This complex migration requires --release-history-max=2 for full validation" }} +{{- end }} +``` + +## Reference Implementation + +A future pull request will: + +1. Extend template rendering context to include `.Release.History` +2. Populate `.Release.History` from release records during upgrade/rollback (reverse chronological order) +3. Add `--release-history-max` flag (default: 0) +4. Add opt-in flag: `--include-history-values` +5. Filter release objects by default to include: Chart, Name, Namespace, Revision, Status, FirstDeployed, LastDeployed +6. When opt-in flag is used, include Values in historical releases +7. Include comprehensive unit and integration tests covering flag behavior, filtering, and edge cases + +## Rejected Ideas + +- **Full Release Object:** Security/performance concerns; filtered release metadata sufficient +- **Chart Metadata Only:** Missing release status/revision limits utility for migration logic +- **Only Version Strings:** Inconsistent with `.Chart`; prevents access to other metadata +- **`.DeployedChart`/`.DeployedCharts` Naming:** Less discoverable than extending `.Release` object +- **Default Max of 1:** Opt-in by default (max 0) is more conservative and safer +- **Environment Variable Control:** Less explicit than CLI flag +- **Cluster Query During `helm template`:** Violates cluster-agnostic design principle +- **Mutable Objects:** Violates read-only template model; no clear use case +- **Separate `--disable-release-history` Flag:** Unified `--release-history-max` with 0 value is cleaner +- **Unlimited History:** Performance implications; requiring explicit max prevents accidental overhead +- **Including Values/Manifests by Default:** While historical values could be useful for migration scenarios, and users already have access via `helm get values --revision N` or `lookup()`, making them automatically available in templates creates additional surface area for accidental exposure. The filtered metadata approach with opt-in flag (`--include-history-values`) serves both conservative defaults and advanced use cases. Manifests moved to future consideration (`--include-history-manifests`) as they are very large and less commonly needed. +- **Including Templates:** Templates are static per chart version; if you need old templates, retrieve the old chart version. No flag needed. +- **`.Release.HistoryMax` Field:** Redundant with `len .Release.History` and `.Release.Revision`. Chart authors can use `.Release.Revision > 1` to detect upgrades and `len .Release.History` to check available data. + +## References + +- [Helm Built-in Objects](https://helm.sh/docs/chart_template_guide/builtin_objects/) +- [Helm Chart.yaml](https://helm.sh/docs/topics/charts/#the-chartyaml-file) +- [Go Templates](https://pkg.go.dev/text/template) +- [Semantic Versioning](https://semver.org/) +- [Example of current workaround](https://github.com/helm/community/pull/421#issuecomment-3662769874)