|
| 1 | +--- |
| 2 | +hip: 9999 |
| 3 | +title: "Expose Release History During Template Rendering" |
| 4 | +authors: ["Andrew Shoell <mrlunchbox777@gmail.com>"] |
| 5 | +created: "2025-11-12" |
| 6 | +type: "feature" |
| 7 | +status: "draft" |
| 8 | +--- |
| 9 | + |
| 10 | +## Abstract |
| 11 | + |
| 12 | +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. |
| 13 | + |
| 14 | +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. |
| 15 | + |
| 16 | +## Motivation |
| 17 | + |
| 18 | +### Current Limitations |
| 19 | + |
| 20 | +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: |
| 21 | + |
| 22 | +**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. |
| 23 | + |
| 24 | +**Pre-Upgrade Hooks:** Store version metadata in ConfigMaps via hooks, creating ordering dependencies and potential failure points. |
| 25 | + |
| 26 | +**Manual Values:** Require users to specify previous versions in values files—error-prone and defeats Helm's release tracking. |
| 27 | + |
| 28 | +### Real-World Impact |
| 29 | + |
| 30 | +This limitation prevents or complicates legitimate use cases: |
| 31 | + |
| 32 | +- **Breaking Changes:** No clean migration path for renamed resources or changed structures |
| 33 | +- **Conditional Resources:** Cannot create migration Jobs based on version deltas |
| 34 | +- **Smart Defaults:** Cannot distinguish fresh installs from upgrades for intelligent defaults |
| 35 | +- **Advanced Deployments:** Blue-green and similar strategies require external orchestration |
| 36 | + |
| 37 | +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. |
| 38 | + |
| 39 | +## Rationale |
| 40 | + |
| 41 | +### Naming: `.Release.History` |
| 42 | + |
| 43 | +`.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. |
| 44 | + |
| 45 | +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. |
| 46 | + |
| 47 | +Alternatives considered and rejected: |
| 48 | + |
| 49 | +- `.PreviousChart` - Ambiguous during rollbacks |
| 50 | +- `.DeployedChart`/`.DeployedCharts` - Less discoverable, doesn't extend existing objects |
| 51 | +- `.InstalledChart` - Confusing with current installation |
| 52 | +- `.CurrentChart` - Ambiguous which is "current" |
| 53 | +- `.Release.Deployed.Chart` - Unnecessarily nested |
| 54 | + |
| 55 | +### Always Available as Template Object |
| 56 | + |
| 57 | +`.Release.History` (empty array or populated) is always present to ensure consistent template behavior, prevent undefined variable errors, and enable testing with `helm template`. |
| 58 | + |
| 59 | +### Populated Only During Upgrades/Rollbacks |
| 60 | + |
| 61 | +`.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: |
| 62 | + |
| 63 | +- `helm install` - No deployed release |
| 64 | +- `helm template` / dry-runs - No cluster context |
| 65 | +- When `--release-history-max 0` is used (default) |
| 66 | + |
| 67 | +### Filtered Release Data |
| 68 | + |
| 69 | +This proposal exposes a filtered subset of release data to balance utility with performance and security: |
| 70 | + |
| 71 | +**Included by default:** |
| 72 | + |
| 73 | +- Chart metadata (Name, Version, AppVersion, and other Chart.yaml fields) |
| 74 | +- Release metadata (Name, Namespace, Revision, Status) |
| 75 | +- Timestamps (FirstDeployed, LastDeployed) |
| 76 | + |
| 77 | +**Excluded by default (opt-in via flag):** |
| 78 | + |
| 79 | +- Values (may contain secrets; use `--include-history-values` to include) |
| 80 | +- Manifests (can be very large; future consideration) |
| 81 | +- Templates (can be very large; no clear use case - templates are static per chart version) |
| 82 | +- Hooks (implementation detail) |
| 83 | + |
| 84 | +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. |
| 85 | + |
| 86 | +**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. |
| 87 | + |
| 88 | +### Max Control Flag |
| 89 | + |
| 90 | +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. |
| 91 | + |
| 92 | +### Design Decisions |
| 93 | + |
| 94 | +- **Different Chart Names:** Still populates `.Release.History` even if chart names differ—templates can detect and handle this |
| 95 | +- **Helm's Record:** Reflects Helm's stored release record, not actual cluster state (use `lookup()` for that) |
| 96 | +- **Dry-Run/Template:** Always empty array to maintain cluster-agnostic, deterministic behavior |
| 97 | +- **Opt-In by Default:** Default max of 0 requires explicit user choice, preventing accidental performance impact |
| 98 | + |
| 99 | +## Specification |
| 100 | + |
| 101 | +### New Template Objects |
| 102 | + |
| 103 | +**`.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). |
| 104 | + |
| 105 | +**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. |
| 106 | + |
| 107 | +**Release Object Structure:** Each release object contains: |
| 108 | + |
| 109 | +```go |
| 110 | +type HistoricalRelease struct { |
| 111 | + Name string // Release name |
| 112 | + Namespace string // Release namespace |
| 113 | + Revision int // Revision number |
| 114 | + Status string // Release status (deployed, superseded, failed, etc.) |
| 115 | + Chart Chart // Chart metadata (same structure as .Chart) |
| 116 | + FirstDeployed time.Time // When this release was first deployed |
| 117 | + LastDeployed time.Time // When this release was last deployed |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +**Usage Examples:** |
| 122 | + |
| 123 | +```yaml |
| 124 | +# Check if upgrading from a version that needs migration |
| 125 | +{{- if gt (len .Release.History) 0 }} |
| 126 | +{{- $lastRelease := index .Release.History 0 }} |
| 127 | +{{- if and (semverCompare ">=2.0.0" .Chart.Version) (semverCompare "<2.0.0" $lastRelease.Chart.Version) }} |
| 128 | +apiVersion: batch/v1 |
| 129 | +kind: Job |
| 130 | +metadata: |
| 131 | + name: {{ include "mychart.fullname" . }}-migration |
| 132 | + annotations: |
| 133 | + "helm.sh/hook": pre-upgrade |
| 134 | +spec: |
| 135 | + template: |
| 136 | + spec: |
| 137 | + containers: |
| 138 | + - name: migrate |
| 139 | + image: myapp/migrator:{{ .Chart.AppVersion }} |
| 140 | + command: ["migrate", "v1-to-v2"] |
| 141 | +{{- end }} |
| 142 | +{{- end }} |
| 143 | + |
| 144 | +# Require minimum depth for safe upgrades |
| 145 | +{{- if lt .Release.HistoryDepth 3 }} |
| 146 | + {{- fail "This chart requires --release-history-depth=3 for safe upgrades from v2.x" }} |
| 147 | +{{- end }} |
| 148 | + |
| 149 | +# Multi-version migration: handle complex upgrade paths |
| 150 | +{{- range .Release.History }} |
| 151 | +{{- if and (eq .Status "deployed") (semverCompare "<1.5.0" .Chart.Version) }} |
| 152 | +# Run migration for successfully deployed versions before 1.5.0 |
| 153 | +{{- end }} |
| 154 | +{{- end }} |
| 155 | + |
| 156 | +# Check if last deployment was successful |
| 157 | +{{- if gt (len .Release.History) 0 }} |
| 158 | +{{- $lastRelease := index .Release.History 0 }} |
| 159 | +{{- if ne $lastRelease.Status "deployed" }} |
| 160 | + {{- fail (printf "Previous release was %s - manual intervention required" $lastRelease.Status) }} |
| 161 | +{{- end }} |
| 162 | +{{- end }} |
| 163 | +``` |
| 164 | + |
| 165 | +### Command-Line Flag |
| 166 | + |
| 167 | +```bash |
| 168 | +# Default: no history retrieved (max 0) |
| 169 | +helm upgrade myrelease mychart |
| 170 | + |
| 171 | +# Retrieve latest deployed release |
| 172 | +helm upgrade myrelease mychart --release-history-max 1 |
| 173 | + |
| 174 | +# Retrieve last 3 releases |
| 175 | +helm upgrade myrelease mychart --release-history-max 3 |
| 176 | + |
| 177 | +# Explicitly disable (same as default) |
| 178 | +helm upgrade myrelease mychart --release-history-max 0 |
| 179 | +``` |
| 180 | + |
| 181 | +### Behavior Matrix |
| 182 | + |
| 183 | +The following table shows what values are available in template context for different operations: |
| 184 | + |
| 185 | +| Operation | `.Release.History` | `.Release.Revision` | |
| 186 | +| ------------------------------------------------- | ------------------------------------- | ------------------- | |
| 187 | +| `helm install` | `[]` | 1 | |
| 188 | +| `helm upgrade` (first) | `[]` (default, no flag) | 2 | |
| 189 | +| `helm upgrade --release-history-max 1` (first) | Populated with 1 release | 2 | |
| 190 | +| `helm upgrade --release-history-max N` | Up to N releases | varies | |
| 191 | +| `helm rollback --release-history-max N` | Populated with releases before target | varies | |
| 192 | +| `helm template` / dry-runs | `[]` | 1 | |
| 193 | + |
| 194 | +**Note:** Use `.Release.Revision` to distinguish installs (revision=1) from upgrades (revision>1). |
| 195 | + |
| 196 | +## Backwards Compatibility |
| 197 | + |
| 198 | +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. |
| 199 | + |
| 200 | +## Security Implications |
| 201 | + |
| 202 | +**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). |
| 203 | + |
| 204 | +**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. |
| 205 | + |
| 206 | +## How to Teach This |
| 207 | + |
| 208 | +### Documentation Additions |
| 209 | + |
| 210 | +1. **Template Objects Reference:** Add `.Release.History` to built-in objects documentation with availability details and usage patterns with `.Release.Revision` |
| 211 | +2. **Upgrade Guide:** "Implementing Version-Aware Upgrades" covering empty array checks, version comparisons, status checking, and best practices |
| 212 | +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) |
| 213 | +4. **Performance Note:** Document that `--release-history-max` should be kept minimal; opt-in by default protects users |
| 214 | +5. **Chart Linting:** Update `helm lint` to warn on `.Release.History` usage without empty array checks, and suggest using `.Release.Revision` for upgrade detection |
| 215 | +6. **Security Guide:** Document the opt-in flag `--include-history-values` with clear warnings about security implications |
| 216 | + |
| 217 | +### Key Example Pattern |
| 218 | + |
| 219 | +```yaml |
| 220 | +# Pattern 1: Defensive check before accessing history |
| 221 | +{{- if gt (len .Release.History) 0 }} |
| 222 | +{{- $lastRelease := index .Release.History 0 }} |
| 223 | +{{- if semverCompare "<3.0.0" $lastRelease.Chart.Version }} |
| 224 | +# Handle breaking change from versions < 3.0.0 |
| 225 | +{{- end }} |
| 226 | +{{- end }} |
| 227 | + |
| 228 | +# Pattern 2: Check only last successful deployment |
| 229 | +{{- $lastSuccessful := dict }} |
| 230 | +{{- range .Release.History }} |
| 231 | + {{- if eq .Status "deployed" }} |
| 232 | + {{- $lastSuccessful = . }} |
| 233 | + {{- break }} |
| 234 | + {{- end }} |
| 235 | +{{- end }} |
| 236 | +{{- if $lastSuccessful }} |
| 237 | + # Use $lastSuccessful for migration logic |
| 238 | +{{- end }} |
| 239 | + |
| 240 | +# Pattern 3: Require history for upgrades (not installs) |
| 241 | +{{- if gt .Release.Revision 1 }} |
| 242 | + {{- if eq (len .Release.History) 0 }} |
| 243 | + {{- fail "Upgrades require --release-history-max=1 for continuity checks" }} |
| 244 | + {{- end }} |
| 245 | +{{- end }} |
| 246 | + |
| 247 | +# Pattern 4: Check for sufficient history depth (less common) |
| 248 | +{{- if and (gt .Release.Revision 1) (lt (len .Release.History) 2) }} |
| 249 | + {{- fail "This complex migration requires --release-history-max=2 for full validation" }} |
| 250 | +{{- end }} |
| 251 | +``` |
| 252 | + |
| 253 | +## Reference Implementation |
| 254 | + |
| 255 | +A future pull request will: |
| 256 | + |
| 257 | +1. Extend template rendering context to include `.Release.History` |
| 258 | +2. Populate `.Release.History` from release records during upgrade/rollback (reverse chronological order) |
| 259 | +3. Add `--release-history-max` flag (default: 0) |
| 260 | +4. Add opt-in flag: `--include-history-values` |
| 261 | +5. Filter release objects by default to include: Chart, Name, Namespace, Revision, Status, FirstDeployed, LastDeployed |
| 262 | +6. When opt-in flag is used, include Values in historical releases |
| 263 | +7. Include comprehensive unit and integration tests covering flag behavior, filtering, and edge cases |
| 264 | + |
| 265 | +## Rejected Ideas |
| 266 | + |
| 267 | +- **Full Release Object:** Security/performance concerns; filtered release metadata sufficient |
| 268 | +- **Chart Metadata Only:** Missing release status/revision limits utility for migration logic |
| 269 | +- **Only Version Strings:** Inconsistent with `.Chart`; prevents access to other metadata |
| 270 | +- **`.DeployedChart`/`.DeployedCharts` Naming:** Less discoverable than extending `.Release` object |
| 271 | +- **Default Max of 1:** Opt-in by default (max 0) is more conservative and safer |
| 272 | +- **Environment Variable Control:** Less explicit than CLI flag |
| 273 | +- **Cluster Query During `helm template`:** Violates cluster-agnostic design principle |
| 274 | +- **Mutable Objects:** Violates read-only template model; no clear use case |
| 275 | +- **Separate `--disable-release-history` Flag:** Unified `--release-history-max` with 0 value is cleaner |
| 276 | +- **Unlimited History:** Performance implications; requiring explicit max prevents accidental overhead |
| 277 | +- **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. |
| 278 | +- **Including Templates:** Templates are static per chart version; if you need old templates, retrieve the old chart version. No flag needed. |
| 279 | +- **`.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. |
| 280 | + |
| 281 | +## References |
| 282 | + |
| 283 | +- [Helm Built-in Objects](https://helm.sh/docs/chart_template_guide/builtin_objects/) |
| 284 | +- [Helm Chart.yaml](https://helm.sh/docs/topics/charts/#the-chartyaml-file) |
| 285 | +- [Go Templates](https://pkg.go.dev/text/template) |
| 286 | +- [Semantic Versioning](https://semver.org/) |
| 287 | +- [Example of current workaround](https://github.com/helm/community/pull/421#issuecomment-3662769874) |
0 commit comments