mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-06 17:41:27 +02:00
* feat: add `inherits:` for sub-helmfile config inheritance
Add an opt-in `inherits:` field to `helmfiles:` entries so a sub-helmfile
can inherit specific configuration categories from its parent:
helmfiles:
- path: myapp.yaml
inherits: [repositories, environments]
Allowed values: repositories, helmDefaults, commonLabels, apiVersions,
kubeVersion, templates, environments. Child values win; parent fills gaps
(consistent with `bases:`). This directly fixes #1495, where a repository
declared in the parent was unavailable to sub-helmfiles, producing a
confusing "repo not found" error.
Implementation notes:
- The 6 pure fields (repositories, helmDefaults, commonLabels, apiVersions,
kubeVersion, templates) are merged post-load via MergeInherited; verified
all are consumed post-load (ExecuteTemplates/converge), never at parse.
- environments is injected pre-load as ctxEnv, because RenderedValues is
baked at load time; the parent's resolved values become the base and the
child's own environments: block overrides per key.
- helmDefaults uses a *HelmSpec pointer (value type is non-comparable) with
a no-override mergo merge, so a child that omits helmDefaults inherits the
parent's fully.
- A footgun warning (WarnUninheritedRepos) suggests
`inherits: [repositories]` when a release references a repo the parent
declares but the child lacks.
- Unknown inherits keys are rejected at parse time with the allowed set.
Inheritance is opt-in and fully backward compatible: empty (the default)
preserves the historical independent-sub-helmfile behavior.
Fixes #1495
Signed-off-by: yxxhero <aiopsclub@163.com>
* fix: address review — deep-copy inherited config and fix bases: doc link
- BuildInheritedConfig now deep-copies the pure fields via a YAML round-trip
(Env via environment.DeepCopy) so the returned config never aliases the
parent state's slices/maps, matching its doc comment. Now returns an error
to surface round-trip failures; the call site in processNestedHelmfiles is
updated. Added TestBuildInheritedConfig_PureFieldsAreDeepCopied to lock
in the no-aliasing guarantee.
- Fix the broken `bases:` anchor (#) in shared-configuration-across-teams.md
to point to writing-helmfile.md#layering-state-files.
Signed-off-by: yxxhero <aiopsclub@163.com>
* fix: address review — reject inherits without path and document helmDefaults caveat
Signed-off-by: yxxhero <aiopsclub@163.com>
* fix: address review — make AllowedInherits immutable and clarify effective-repo wording
Signed-off-by: yxxhero <aiopsclub@163.com>
---------
Signed-off-by: yxxhero <aiopsclub@163.com>
243 lines
7.3 KiB
Markdown
243 lines
7.3 KiB
Markdown
# Shared Configuration Across Teams
|
|
|
|
Assume you have two or more teams, each work for a different internal or external service, like:
|
|
|
|
- Product 1
|
|
- Product 2
|
|
- Observability
|
|
|
|
The simplest `helmfile.yaml` that declares the whole cluster that is composed of the three services would look like the below:
|
|
|
|
```yaml
|
|
releases:
|
|
- name: product1-api
|
|
chart: product1-charts/api
|
|
# snip
|
|
- name: product1-web
|
|
chart: product1-charts/web
|
|
# snip
|
|
- name: product2-api
|
|
chart: saas-charts/api
|
|
# snip
|
|
- name: product2-web
|
|
chart: product2-charts/web
|
|
# snip
|
|
- name: observability-prometheus-operator
|
|
chart: stable/prometheus-operator
|
|
# snip
|
|
- name: observability-process-exporter
|
|
chart: stable/prometheus-operator
|
|
# snip
|
|
```
|
|
|
|
This works, but what if you wanted to a separate cluster per service to achieve a smaller blast radius?
|
|
|
|
Let's start by creating a `helmfile.yaml` for each service.
|
|
|
|
`product1/helmfile.yaml`:
|
|
|
|
```yaml
|
|
releases:
|
|
- name: product1-api
|
|
chart: product1-charts/api
|
|
# snip
|
|
- name: product1-web
|
|
chart: product1-charts/web
|
|
# snip
|
|
- name: observability-prometheus-operator
|
|
chart: stable/prometheus-operator
|
|
# snip
|
|
- name: observability-process-exporter
|
|
chart: stable/prometheus-operator
|
|
# snip
|
|
```
|
|
|
|
`product2/helmfile.yaml`:
|
|
|
|
```yaml
|
|
releases:
|
|
- name: product2-api
|
|
chart: product2-charts/api
|
|
# snip
|
|
- name: product2-web
|
|
chart: product2-charts/web
|
|
# snip
|
|
- name: observability-prometheus-operator
|
|
chart: stable/prometheus-operator
|
|
# snip
|
|
- name: observability-process-exporter
|
|
chart: stable/prometheus-operator
|
|
# snip
|
|
```
|
|
|
|
You will (of course!) notice this isn't DRY.
|
|
|
|
To remove the duplication of observability stack between the two helmfiles, create a "sub-helmfile" for the observability stack.
|
|
|
|
`observability/helmfile.yaml`:
|
|
|
|
```yaml
|
|
- name: observability-prometheus-operator
|
|
chart: stable/prometheus-operator
|
|
# snip
|
|
- name: observability-process-exporter
|
|
chart: stable/prometheus-operator
|
|
# snip
|
|
```
|
|
|
|
As you might have imagined, the observability helmfile can be reused from the two product helmfiles by declaring `helmfiles`.
|
|
|
|
`product1/helmfile.yaml`:
|
|
|
|
```yaml
|
|
helmfiles:
|
|
- ../observability/helmfile.yaml
|
|
|
|
releases:
|
|
- name: product1-api
|
|
chart: product1-charts/api
|
|
# snip
|
|
- name: product1-web
|
|
chart: product1-charts/web
|
|
# snip
|
|
```
|
|
|
|
`product2/helmfile.yaml`:
|
|
|
|
```yaml
|
|
helmfiles:
|
|
- ../observability/helmfile.yaml
|
|
|
|
releases:
|
|
- name: product2-api
|
|
chart: product2-charts/api
|
|
# snip
|
|
- name: product2-web
|
|
chart: product2-charts/web
|
|
# snip
|
|
```
|
|
|
|
## Using sub-helmfile as a template
|
|
|
|
You can go even further by generalizing the product related releases as a pair of `api` and `web`:
|
|
|
|
`shared/helmfile.yaml`:
|
|
|
|
```yaml
|
|
releases:
|
|
- name: product{{ env "PRODUCT_ID" }}-api
|
|
chart: product{{ env "PRODUCT_ID" }}-charts/api
|
|
# snip
|
|
- name: product{{ env "PRODUCT_ID" }}-web
|
|
chart: product{{ env "PRODUCT_ID" }}-charts/web
|
|
# snip
|
|
```
|
|
|
|
Then you only need one single product helmfile
|
|
|
|
|
|
`product/helmfile.yaml`:
|
|
|
|
```yaml
|
|
helmfiles:
|
|
- ../observability/helmfile.yaml
|
|
- ../shared/helmfile.yaml
|
|
```
|
|
|
|
Now that we use the environment variable `PRODUCT_ID` to as the parameters of release names, you need to set it before running `helmfile`, so that it produces the differently named releases per product:
|
|
|
|
```console
|
|
$ PRODUCT_ID=1 helmfile -f product/helmfile.yaml apply
|
|
$ PRODUCT_ID=2 helmfile -f product/helmfile.yaml apply
|
|
```
|
|
|
|
## Inheriting parent configuration with `inherits:`
|
|
|
|
By default a sub-helmfile is independent: it does **not** see the `repositories:`,
|
|
`helmDefaults:`, `environments:`, etc. declared in the parent that includes it. To
|
|
share configuration you either extract it into a separate file referenced via
|
|
[`bases:`](writing-helmfile.md#layering-state-files) from each sub-helmfile, or — more concisely — let each sub-helmfile
|
|
opt into inheriting specific categories from its parent with `inherits:`.
|
|
|
|
```yaml
|
|
# parent helmfile.yaml
|
|
repositories:
|
|
- name: release-charts
|
|
url: registry.example.com/release/helm-charts
|
|
oci: true
|
|
|
|
helmfiles:
|
|
- path: myapp.yaml
|
|
inherits:
|
|
- repositories
|
|
- environments
|
|
```
|
|
|
|
`myapp.yaml` now sees the `release-charts` repository and the parent's resolved
|
|
environment values, without having to re-declare them.
|
|
|
|
### Allowed values
|
|
|
|
`inherits:` accepts a list. Each entry must be one of:
|
|
|
|
| Key | What is inherited |
|
|
|-----|-------------------|
|
|
| `repositories` | Parent repository definitions (appended; child entry wins on name conflict). |
|
|
| `helmDefaults` | Parent helm defaults; parent fills the child's unset sub-fields, child's set sub-fields win (see caveat below). |
|
|
| `commonLabels` | Parent common labels; child's keys win on conflict. |
|
|
| `apiVersions` | Parent API versions (appended and de-duplicated). |
|
|
| `kubeVersion` | Parent kube version, only when the child left it empty. |
|
|
| `templates` | Parent templates; child's template wins on name conflict. |
|
|
| `environments` | The parent's **resolved** environment values (see note below). |
|
|
|
|
### Precedence
|
|
|
|
**Child wins, parent fills gaps** — consistent with `bases:`. Slices like
|
|
`repositories` accumulate (parent first); maps (`commonLabels`, `templates`) are
|
|
unioned with the child's keys winning; `helmDefaults` is deep-merged so the child
|
|
overrides individual sub-fields it sets.
|
|
|
|
#### Caveat: `helmDefaults` and zero values
|
|
|
|
`helmDefaults` is deep-merged field-by-field, and Go value types cannot
|
|
distinguish an explicitly-set zero value from "unset". So a child that sets a
|
|
sub-field to its zero value (for example `atomic: false` to disable an inherited
|
|
`atomic: true`) will still see the parent's non-zero value fill in. To override
|
|
such a field, set it to a non-zero value, or omit `helmDefaults` from `inherits:`
|
|
and declare the block in full on the child. The common case — a child that omits
|
|
`helmDefaults` entirely and inherits the parent's — is unaffected.
|
|
|
|
### Note on `environments`
|
|
|
|
`environments` inherits the parent's **already-resolved** values (including CLI
|
|
overrides and decrypted secrets), not the `environments:` declaration block. This
|
|
avoids ambiguity around the directory that relative values files resolve against
|
|
(the parent resolves them once). The child's own `environments:` block, if any,
|
|
still takes precedence per key.
|
|
|
|
### Transitive inheritance
|
|
|
|
Inheritance is opt-in at **every** level but the values propagate. If a parent
|
|
inherits `repositories` and the child in turn has its own `helmfiles:` entries
|
|
declaring `inherits: [repositories]`, the grandchild receives the accumulated set.
|
|
|
|
### `inherits:` vs `bases:`
|
|
|
|
| | `bases:` | `inherits:` |
|
|
|---|---|---|
|
|
| Model | Pull — each file lists files to merge into itself | Push — the parent pushes its config to the child |
|
|
| Where the shared config lives | Must live in a separate file | Can live inline in the parent |
|
|
| Repetition | `bases:` must be repeated in every sub-helmfile | Declared once, per sub-helmfile entry |
|
|
|
|
They are complementary: use `bases:` for cross-team reusable building blocks
|
|
(environment defaults, shared helm defaults), and `inherits:` to let a sub-helmfile
|
|
consume the parent's inline configuration without duplication.
|
|
|
|
### Footgun warning
|
|
|
|
If a sub-helmfile references a repository that the parent declares but the child
|
|
does not (and `repositories` is not inherited), helmfile prints a warning suggesting
|
|
`inherits: [repositories]` instead of failing later with a confusing
|
|
`repo not found` (see [#1495](https://github.com/helmfile/helmfile/issues/1495)).
|
|
|