kubernetes_template_rendering 0.2.5 → 0.6.0.prerelease.tstarck.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +52 -4
- data/docs/adrs/0001-strict-rendering-paths-for-stale-resource-deletion.md +98 -0
- data/docs/adrs/0002-spp-aware-reconcile-scopes-and-only-guard.md +114 -0
- data/lib/kubernetes_template_rendering/cli.rb +27 -1
- data/lib/kubernetes_template_rendering/cli_arguments.rb +8 -1
- data/lib/kubernetes_template_rendering/placeholder_expander.rb +37 -0
- data/lib/kubernetes_template_rendering/reconciler.rb +114 -0
- data/lib/kubernetes_template_rendering/resource_set.rb +96 -4
- data/lib/kubernetes_template_rendering/template.rb +2 -10
- data/lib/kubernetes_template_rendering/template_directory_renderer.rb +149 -22
- data/lib/kubernetes_template_rendering/version.rb +1 -1
- metadata +20 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1306190d836dbde84a594aee2b814d7db1238f0863f4fd8a9c0ccea2979a46b3
|
|
4
|
+
data.tar.gz: dbae4fa4255214b4029227ad9e3bc48f73de0ba248f03cba7d584bfbdebc0dd1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85fbba190072912153255c4e06af9c3562613318d6c7cc0f14bf4e9d7f574015d2741bf17a85d3080d4f89c3cfdaf818d7d86e7c07c84d8294f764a370938864
|
|
7
|
+
data.tar.gz: ff0c9c764b719631f4fa250d0fb948203d2b1d57016de81aee53ae2c5febabf9e2f2266ce6d703a8c01f356993d77d143f2edd9f2d13d12e492829eee3c31fed
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
4
4
|
|
|
5
5
|
Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.6.0] - Unreleased
|
|
8
|
+
### Added
|
|
9
|
+
- Added `--reconcile` flag: a bounded, marker-based sweep that replaces the destructive per-entry `rm -rf` of `--prune`. It touches a marker before rendering, then after rendering deletes only files older than the marker under each scope root (`<region>/<cluster_type>/<color>/`) and removes empty directories, correctly cleaning up directories of deleted/renamed entries. `spp/` subtrees are fenced out of the base sweep, paths resolving outside their scope prefix raise a hard error, and `--reconcile` combined with `--prune` is rejected.
|
|
10
|
+
- Made `--reconcile` `--spp`-aware: without `--spp` only the `SPP-PLACEHOLDER` subtree is swept; with `--spp NAME` the sweep covers `SPP-PLACEHOLDER` (always re-rendered, as the expansion source) plus each requested per-SPP subtree (substituting `SPP-PLACEHOLDER` into the sweep root), leaving unrequested SPP siblings intact. Rejected `--reconcile` combined with `--only`, which would delete un-rendered siblings under the shared base root. See ADR-0002.
|
|
11
|
+
- Added a `--reconcile` SPP layout guard: SPP entries (name contains `SPP-PLACEHOLDER`) must render under `<region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER/`, and non-SPP entries must not render under any `spp/` segment. A `directory:` override that still resolves to the canonical SPP prefix is allowed; anything else hard-errors before rendering. The guard runs only under `--reconcile`. See ADR-0002.
|
|
12
|
+
|
|
13
|
+
## [0.5.0] - 2026-06-26
|
|
14
|
+
### Added
|
|
15
|
+
- Added `--spp NAME` (repeatable) flag that expands rendered output of `SPP-PLACEHOLDER` entries into per-Staging-Partial-Platform sibling directories, substituting `SPP-PLACEHOLDER` and its `PLACEHOLDER` suffix in both paths and contents. Composes with the SPP-derived base path introduced in 0.4.0 (sibling per-SPP trees are created next to the literal `SPP-PLACEHOLDER` segment). Replaces the post-render `invocaops_docker/tools/spp-transform/spp-transform.rb` step inside the gem.
|
|
16
|
+
- Added `--only NAME` (repeatable) flag that filters rendering to specific top-level `definitions.yaml` entries by exact key match. Composes with `--cluster_type`/`--region`/`--color`/`--spp` (all filters are AND'd). Raises with a list of valid keys if any `--only` value matches no entry across the rendered template directories.
|
|
17
|
+
|
|
18
|
+
## [0.4.0] - 2026-06-25
|
|
19
|
+
### Added
|
|
20
|
+
- Added a `subdirectory:` option to `definitions.yaml`. It is mutually exclusive with `directory:` and sets the output path to the base path `%{plain_region}/%{type}/%{color}/<subdirectory>`. When neither `directory:` nor `subdirectory:` is given, output is rendered to the base path `%{plain_region}/%{type}/%{color}` (previously a missing `directory:` raised an error).
|
|
21
|
+
- Emit a deprecation warning on any use of `directory:` in `definitions.yaml`, suggesting to remove it (to render into the standard `%{plain_region}/%{type}/%{color}` layout) or switch to `subdirectory:`. `directory:` is the only way to produce a non-standard path, which is unsafe for the planned `--reconcile` stale-resource deletion. See ADR-0001.
|
|
22
|
+
- SPP definitions (those whose name contains the `SPP-PLACEHOLDER` token) now render under a derived base path `%{plain_region}/%{type}/%{color}/spp/SPP-PLACEHOLDER`, with `subdirectory:` composing on top. This keeps each SPP instance's output distinct and bounded under the `region/type/color` tree for `--reconcile`, and preserves the literal `SPP-PLACEHOLDER` token for downstream per-instance substitution. The `directory:` deprecation warning now points SPP definitions at the SPP base layout. See ADR-0001.
|
|
23
|
+
|
|
24
|
+
## [0.3.0] - 2026-06-24
|
|
25
|
+
### Fixed
|
|
26
|
+
- Ruby 4.0 compatibility: declare `ostruct` as a dependency (it is `require`d directly but was removed from Ruby's default gems in 4.0.0) and bump `activesupport` to `7.2.3.1` so its `logger` dependency is resolved (`logger` was likewise dropped from default gems).
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
- No longer emit the `# Variable overrides used:` comment in rendered files. Because overrides such as `deploySha` change on every build, this comment caused large, content-free diffs across every rendered file. `--variable-override` still applies the overrides to the rendered output; only the comment is removed.
|
|
30
|
+
|
|
7
31
|
## [0.2.5] - 2025-05-08
|
|
8
32
|
### Fixed
|
|
9
33
|
- Updated `rexml` dependency to avoid security issues
|
data/README.md
CHANGED
|
@@ -7,12 +7,14 @@ configuration for various deployment environments.
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
9
9
|
Install the gem and add to the application's Gemfile by executing:
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
```bash
|
|
11
12
|
bundle add kubernetes_template_rendering
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
15
|
-
|
|
16
|
+
|
|
17
|
+
```bash
|
|
16
18
|
gem install kubernetes_template_rendering
|
|
17
19
|
```
|
|
18
20
|
|
|
@@ -23,20 +25,66 @@ To use this gem you can either install it, and use the `render_kubernetes_templa
|
|
|
23
25
|
`gem exec` to execute the command without first installing the gem.
|
|
24
26
|
|
|
25
27
|
### Example Usage
|
|
28
|
+
|
|
26
29
|
```bash
|
|
27
30
|
gem exec -g kubernetes_template_rendering render_templates \
|
|
28
31
|
--jsonnet-library-path deployment/vendor \
|
|
29
|
-
--
|
|
32
|
+
--rendered-directory path/to/resources \
|
|
30
33
|
deployment/templates
|
|
31
34
|
```
|
|
32
35
|
|
|
33
36
|
### Options
|
|
34
37
|
|
|
35
38
|
To see a full list of options and how to use them, run the following command:
|
|
39
|
+
|
|
36
40
|
```bash
|
|
37
41
|
gem exec -g kubernetes_template_rendering render_templates --help
|
|
38
42
|
```
|
|
39
43
|
|
|
44
|
+
### Cleaning up stale output: `--prune` vs `--reconcile`
|
|
45
|
+
|
|
46
|
+
Both flags remove output left over from templates/entries that no longer render, but they differ in how:
|
|
47
|
+
|
|
48
|
+
- `--prune` deletes each entry's output directory with `rm -rf` **before** rendering. It never removes the directories of fully deleted/renamed entries and can clobber sibling directories when one entry renders at a prefix root above another.
|
|
49
|
+
- `--reconcile` performs a safer, bounded sweep: it touches a marker, renders, then deletes only files older than the marker under each scope root `<region>/<cluster_type>/<color>/` (honoring `--cluster_type` / `--region` / `--color`), and finally removes any now-empty directories. This cleans up directories of deleted/renamed entries without clobbering freshly-rendered siblings, and two identical reconcile runs produce the same result.
|
|
50
|
+
|
|
51
|
+
Notes:
|
|
52
|
+
|
|
53
|
+
- `--reconcile` and `--prune` are mutually exclusive (passing both exits with an error). `--reconcile` and `--only` are likewise mutually exclusive, since a filtered render would leave un-rendered siblings looking stale under the shared base root.
|
|
54
|
+
- `spp/` subtrees are fenced out of the base sweep. Without `--spp`, only the `SPP-PLACEHOLDER` subtree is swept. With `--spp NAME`, reconcile sweeps `SPP-PLACEHOLDER` (always re-rendered, as the expansion source) plus each requested per-SPP subtree, leaving unrequested SPP siblings intact. Deleted-SPP cleanup for unrequested siblings remains a manual `git rm` in the teardown runbook. See ADR-0002.
|
|
55
|
+
- If any rendered entry resolves to a path outside its scope prefix (a full-path or relative `..` escape), reconcile hard-errors before deleting anything.
|
|
56
|
+
- Reconcile enforces the SPP layout: every SPP entry (name contains `SPP-PLACEHOLDER`) must render under `<region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER/`, and no non-SPP entry may render under an `spp/` segment. A `directory:` override that still resolves to that prefix is allowed; anything else hard-errors before rendering. (This guard runs only under `--reconcile`.) See ADR-0002.
|
|
57
|
+
|
|
58
|
+
### Filtering to specific entries
|
|
59
|
+
|
|
60
|
+
Pass `--only NAME` (repeatable) to render only the `definitions.yaml` entries whose top-level key exactly matches `NAME`. Composes with `--cluster_type`/`--region`/`--color`/`--spp` — all filters are AND'd.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
gem exec -g kubernetes_template_rendering render_templates \
|
|
64
|
+
--rendered-directory path/to/resources \
|
|
65
|
+
--cluster_type staging \
|
|
66
|
+
--only staging.test \
|
|
67
|
+
deployment/templates
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Useful when one `--cluster_type` matches multiple sibling entries (e.g. `staging` and `staging.test` both match `--cluster_type staging` after the suffix-strip rule) and you want to render only one of them. Repeated `--only` values are deduped.
|
|
71
|
+
|
|
72
|
+
If an `--only` value matches no entry across any template directory, the gem raises with the list of valid keys so the caller can self-correct.
|
|
73
|
+
|
|
74
|
+
### Staging Partial Platforms
|
|
75
|
+
|
|
76
|
+
Pass `--spp NAME` (repeatable) to expand any entry whose `definitions.yaml` name contains `SPP-PLACEHOLDER` into a per-SPP sibling output. Substitutes `SPP-PLACEHOLDER` with `NAME` and the `PLACEHOLDER` suffix with the suffix of `NAME` (everything after the last `-`), in both file paths and contents. Source mtimes are preserved.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
gem exec -g kubernetes_template_rendering render_templates \
|
|
80
|
+
--rendered-directory path/to/resources \
|
|
81
|
+
--spp staging-qa02a \
|
|
82
|
+
--spp staging-qa08a \
|
|
83
|
+
deployment/templates
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This is purely additive — the placeholder-bearing output tree is still produced, and per-SPP trees are written alongside it. Repeated `--spp` values are deduped.
|
|
87
|
+
|
|
40
88
|
## Development
|
|
41
89
|
|
|
42
90
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -45,4 +93,4 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
45
93
|
|
|
46
94
|
## Contributing
|
|
47
95
|
|
|
48
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/invoca/kubernetes_template_rendering
|
|
96
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/invoca/kubernetes_template_rendering>.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Strict, derived output paths so stale rendered resources can be safely reconciled
|
|
2
|
+
|
|
3
|
+
* Status: accepted
|
|
4
|
+
* Deciders: Tristan Starck, Kubernetes platform reviewers (via PR #17)
|
|
5
|
+
* Date: 2026-06-25
|
|
6
|
+
|
|
7
|
+
Technical Story: OCTO-849 — add a `subdirectory` option to `definitions.yaml` / [PR #17](https://github.com/Invoca/kubernetes_template_rendering/pull/17)
|
|
8
|
+
|
|
9
|
+
## Context and Problem Statement
|
|
10
|
+
|
|
11
|
+
The renderer writes Kubernetes manifests into a `rendered_directory`. We want it to remove *stale* resources — those that are no longer rendered — so the rendered tree always matches the current templates. The current `--prune` flag does this bluntly: it `rm_rf`s an output directory before re-rendering. That is destructive (it deletes everything, then re-creates whatever is still rendered) and is only as safe as the output path is correct — a wrong or tree-escaping path means deleting the wrong files.
|
|
12
|
+
|
|
13
|
+
We are moving to a `--reconcile` flag instead: after rendering the desired set of resources, reconcile compares the rendered output tree against that desired set and deletes only the entries that are no longer rendered. Reconciliation is only safe and correct if the renderer knows exactly which paths it owns and can enumerate the complete desired set within a bounded tree.
|
|
14
|
+
|
|
15
|
+
Today the output location of each resource set is a freeform `directory:` pattern in `definitions.yaml` that can point anywhere, including escaping the rendered tree via `..` (e.g. `../some-cluster/...`). When output paths are arbitrary, the set of directories the renderer owns is not well-defined, so reconcile cannot safely decide what is stale. How do we make rendering deterministic enough to reconcile reliably — deleting resources that are no longer rendered, and nothing else?
|
|
16
|
+
|
|
17
|
+
## Decision Drivers
|
|
18
|
+
|
|
19
|
+
* **Safe reconciliation of stale resources** — the core motivation: `--reconcile` must delete only resources the renderer owns and no longer renders.
|
|
20
|
+
* **Deterministic, bounded output paths** — output must be predictable and rooted inside the rendered tree, never escaping it, so the owned set is enumerable.
|
|
21
|
+
* **Lower boilerplate** — the `%{plain_region}/%{type}/%{color}` prefix is repeated in nearly every definition.
|
|
22
|
+
* **Backward compatibility** — existing `definitions.yaml` files that use `directory:` must keep working.
|
|
23
|
+
* **Hard to misuse** — the common case should require the least configuration and offer the fewest footguns.
|
|
24
|
+
|
|
25
|
+
## Considered Options
|
|
26
|
+
|
|
27
|
+
* **Option 1 — Strict, derived paths.** Output is always `%{plain_region}/%{type}/%{color}[/<subdirectory>]`. Introduce a `subdirectory:` field, make the prefix the default, and deprecate/restrict the freeform `directory:` field.
|
|
28
|
+
* **Option 2 — Keep freeform `directory:` only.** Status quo; document conventions but enforce nothing.
|
|
29
|
+
* **Option 3 — Freeform `directory:` plus a rendered-path manifest.** Keep arbitrary layouts but emit an out-of-band manifest of rendered paths that `--reconcile` consults.
|
|
30
|
+
|
|
31
|
+
## Decision Outcome
|
|
32
|
+
|
|
33
|
+
Chosen option: **Option 1 — strict, derived output paths.**
|
|
34
|
+
|
|
35
|
+
Rendered output paths MUST be derived from `plain_region`, the kubernetes cluster type (`type`), and `color`. The output directory of a resource set is resolved from the mutually-exclusive `directory:` and `subdirectory:` fields:
|
|
36
|
+
|
|
37
|
+
| Config present | Resulting output-directory pattern |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `directory` only | the `directory` value, verbatim (legacy / escape hatch) |
|
|
40
|
+
| `subdirectory` only | `%{plain_region}/%{type}/%{color}/<subdirectory>` |
|
|
41
|
+
| `subdirectory` only, SPP definition | `%{plain_region}/%{type}/%{color}/spp/SPP-PLACEHOLDER/<subdirectory>` |
|
|
42
|
+
| neither, SPP definition | `%{plain_region}/%{type}/%{color}/spp/SPP-PLACEHOLDER` |
|
|
43
|
+
| neither | `%{plain_region}/%{type}/%{color}` (base path) |
|
|
44
|
+
| both | error (`ArgumentError`) |
|
|
45
|
+
|
|
46
|
+
`subdirectory:` is a plain literal final segment appended to the base path — no `%{...}` interpolation is performed on the value itself. The resolved pattern still flows through the existing per-region/color `format(...)` step, so e.g. `subdirectory: my-app` with region `us-east-1`, cluster type `prod`, and color `orange` renders into `us-east-1/prod/orange/my-app`. When neither field is given, output goes to the base path (previously a missing `directory:` raised an error).
|
|
47
|
+
|
|
48
|
+
Definitions whose name contains the `SPP-PLACEHOLDER` token (the same token from
|
|
49
|
+
which the `staging` cluster type is derived) render under an additional
|
|
50
|
+
`spp/SPP-PLACEHOLDER` segment: their base path is
|
|
51
|
+
`%{plain_region}/%{type}/%{color}/spp/SPP-PLACEHOLDER`, and `subdirectory:` composes
|
|
52
|
+
on top of it. The `spp/SPP-PLACEHOLDER` segment is literal (not substituted by
|
|
53
|
+
`format`), so the token survives into the rendered tree for downstream per-instance
|
|
54
|
+
substitution while remaining bounded under the `region/type/color` tree. The
|
|
55
|
+
warn-only-then-reject rollout for non-conforming `directory:` applies equally to SPP
|
|
56
|
+
definitions.
|
|
57
|
+
|
|
58
|
+
Because the base-path and `subdirectory` outputs are deterministic and rooted under the `region/type/color` tree, the renderer owns a predictable, enumerable set of directories. That is the property that lets `--reconcile` compute the desired set, diff it against what is on disk, and delete only the stale entries — safely and without escaping the tree.
|
|
59
|
+
|
|
60
|
+
Additionally, the freeform `directory:` field is **deprecated**. It remains supported for backward compatibility, but it is the only way to produce a non-conforming (or tree-escaping) path, so new definitions should use `subdirectory:` or the base-path default. As a first step, the renderer now **warns on any use of `directory:`**, suggesting authors remove it (to render into the standard `%{plain_region}/%{type}/%{color}` layout) or switch to `subdirectory:`. A subsequent follow-up will upgrade this to hard validation/removal so that legacy usage cannot break reconcile safety. **(Rejection not yet enforced — currently warn-only.)**
|
|
61
|
+
|
|
62
|
+
### Positive Consequences
|
|
63
|
+
|
|
64
|
+
* `--reconcile` can safely delete stale resources because the renderer's output directories are deterministic and bounded.
|
|
65
|
+
* Less boilerplate — the common `region/type/color` prefix is supplied automatically.
|
|
66
|
+
* The misconfiguration surface shrinks over time as `directory:` usage is retired.
|
|
67
|
+
|
|
68
|
+
### Negative Consequences
|
|
69
|
+
|
|
70
|
+
* **Behavior change:** a missing `directory:` no longer raises; it now renders to the base path. The previous "missing `directory:`" error is gone.
|
|
71
|
+
* Use of `directory:` is currently only a warning, not an error, so until the follow-up hard validation lands the reconcile-safety guarantee is not yet airtight.
|
|
72
|
+
* Deprecating `directory:` requires migrating existing definitions over time.
|
|
73
|
+
|
|
74
|
+
## Pros and Cons of the Options
|
|
75
|
+
|
|
76
|
+
### Option 1 — Strict, derived paths (chosen)
|
|
77
|
+
|
|
78
|
+
* Good, because deterministic, bounded paths make `--reconcile` / stale-resource deletion safe.
|
|
79
|
+
* Good, because it removes the repeated `region/type/color` prefix boilerplate.
|
|
80
|
+
* Good, because it is backward compatible — `directory:` still works while we steer toward strict paths.
|
|
81
|
+
* Bad, because the full guarantee is deferred until `directory:` is validated/retired.
|
|
82
|
+
|
|
83
|
+
### Option 2 — Keep freeform `directory:` only
|
|
84
|
+
|
|
85
|
+
* Good, because no migration is required and layouts stay maximally flexible.
|
|
86
|
+
* Bad, because output ownership is undefined, so reconcile cannot safely decide what is stale.
|
|
87
|
+
* Bad, because patterns can escape the rendered tree via `..`.
|
|
88
|
+
|
|
89
|
+
### Option 3 — Freeform `directory:` plus a rendered-path manifest
|
|
90
|
+
|
|
91
|
+
* Good, because it decouples reconcile from path layout and could track arbitrary layouts.
|
|
92
|
+
* Bad, because it adds a stateful artifact to generate, store, and trust.
|
|
93
|
+
* Bad, because it is more moving parts than simply enforcing a deterministic layout.
|
|
94
|
+
|
|
95
|
+
## Links
|
|
96
|
+
|
|
97
|
+
* Implemented by [PR #17](https://github.com/Invoca/kubernetes_template_rendering/pull/17) (OCTO-849) — `subdirectory:` field, base-path default, `ResourceSet` directory resolution.
|
|
98
|
+
* Supersedes the implementation spec previously at `docs/superpowers/specs/2026-06-25-subdirectory-option-design.md` (removed; its content is folded into this ADR).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# SPP-aware reconcile scopes, layout guard, and a --reconcile/--only guard
|
|
2
|
+
|
|
3
|
+
* Status: accepted
|
|
4
|
+
* Deciders: Tristan Starck, Kubernetes platform reviewers
|
|
5
|
+
* Date: 2026-06-29 (revised 2026-07-06)
|
|
6
|
+
|
|
7
|
+
Technical Story: OCTO-842 — add a `reconcile` flag to kubernetes_template_rendering / depends on [PR #14](https://github.com/Invoca/kubernetes_template_rendering/pull/14) (`--spp` + `PlaceholderExpander`)
|
|
8
|
+
|
|
9
|
+
## Revision History
|
|
10
|
+
|
|
11
|
+
* **2026-06-29** — original decision: SPP-aware sweep scopes (placeholder substitution) and the `--reconcile`/`--only` hard-error guard.
|
|
12
|
+
* **2026-07-06** — revised the `--spp` sweep scope to *also* sweep `SPP-PLACEHOLDER` (see "`--spp` reconcile scope derivation" below), and folded in the SPP layout guard previously drafted as ADR-0003.
|
|
13
|
+
|
|
14
|
+
## Context and Problem Statement
|
|
15
|
+
|
|
16
|
+
[ADR-0001](0001-strict-rendering-paths-for-stale-resource-deletion.md) established `--reconcile`: after rendering the desired resources, sweep each owned root and delete files older than a marker captured before rendering, then remove empty directories. The base case sweeps `<region>/<cluster_type>/<color>/` with an `spp/` fence so the shared base sweep never touches Staging Partial Platform (SPP) subtrees.
|
|
17
|
+
|
|
18
|
+
SPP definitions render under `<region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER/...`. The literal `SPP-PLACEHOLDER` token is preserved so it can be expanded per-instance downstream. PR #14 introduces the `--spp NAME` flag and `PlaceholderExpander`, which expands the `SPP-PLACEHOLDER` output into one concrete subtree per requested SPP (e.g. `spp/staging-qa02a/`). Crucially, `SPP-PLACEHOLDER` is *always* re-rendered on every run — it is the source `PlaceholderExpander` copies from — so its freshly rendered files are always newer than the reconcile marker.
|
|
19
|
+
|
|
20
|
+
Three questions follow for reconcile:
|
|
21
|
+
|
|
22
|
+
1. When a deploy targets specific SPPs (`--spp staging-qa02a`), which subtrees may reconcile sweep? It must delete stale files inside the *requested* SPP and inside `SPP-PLACEHOLDER` (both re-rendered this run), and must never touch unrequested SPP siblings (their deleted-SPP cleanup stays a manual `git rm` per the teardown runbook).
|
|
23
|
+
2. How should `--reconcile` interact with PR #14's `--only` (render a subset of `definitions.yaml` entries)?
|
|
24
|
+
3. How do we guarantee SPP resources actually land at the canonical `spp/SPP-PLACEHOLDER/` layout the sweep scoping depends on?
|
|
25
|
+
|
|
26
|
+
## Decision Drivers
|
|
27
|
+
|
|
28
|
+
* **Bounded, targeted sweeps** — `--spp` must scope reconcile to exactly the subtrees rendered this run.
|
|
29
|
+
* **No accidental cross-SPP deletion** — unrequested SPP siblings must be fenced out of the sweep.
|
|
30
|
+
* **Sweep the leftovers we own** — `SPP-PLACEHOLDER` is re-rendered every run, so its stale files (from deleted/renamed templates) must be collected, not left to accumulate.
|
|
31
|
+
* **No silent data loss from partial renders** — a flag that renders a subset must not let reconcile delete the un-rendered (and therefore stale-looking) siblings.
|
|
32
|
+
* **Enforce the layout the sweep assumes** — SPP resources must be where the fence-and-substitute scoping expects them.
|
|
33
|
+
* **Minimal coupling to PR #14** — OCTO-842 owns reconcile scoping; expansion is PR #14's. Avoid duplicating `PlaceholderExpander` and keep the merge trivial.
|
|
34
|
+
|
|
35
|
+
## Considered Options
|
|
36
|
+
|
|
37
|
+
### Flag interaction: `--reconcile` + `--only`
|
|
38
|
+
|
|
39
|
+
* **Option A — Hard error.** Reject `--reconcile --only` the same way `--reconcile --prune` is rejected.
|
|
40
|
+
* **Option B — Narrow the sweep to only the rendered entries' roots.** Sweep just the roots of the `--only` entries.
|
|
41
|
+
* **Option C — Allow it as-is.** Run the normal base sweep alongside a subset render.
|
|
42
|
+
|
|
43
|
+
### `--spp` reconcile scope derivation
|
|
44
|
+
|
|
45
|
+
* **Option 1 — Sweep `SPP-PLACEHOLDER` plus one root per requested target.** For each SPP scope whose root contains `SPP-PLACEHOLDER`, sweep the placeholder root itself *and* emit one sweep root per requested target (`spp/staging-qa02a`, …); with no `--spp`, sweep `spp/SPP-PLACEHOLDER` as-is.
|
|
46
|
+
* **Option 1a (original, superseded) — Substitute only, excluding `SPP-PLACEHOLDER`.** Under `--spp`, sweep *only* the requested targets and leave `SPP-PLACEHOLDER` untouched.
|
|
47
|
+
* **Option 2 — Always sweep the whole `spp/` tree** and rely on mtimes alone.
|
|
48
|
+
|
|
49
|
+
### SPP layout enforcement (folded from ADR-0003)
|
|
50
|
+
|
|
51
|
+
* **When:** (A) only under `--reconcile`, or (B) always.
|
|
52
|
+
* **What:** (1) both directions — SPP entries must be under `spp/SPP-PLACEHOLDER/`, non-SPP entries must not be under any `spp/` segment; or (2) forward only.
|
|
53
|
+
* **`directory:` escape hatch:** (X) allow if it still resolves to the canonical prefix; or (Y) reject outright for SPP entries.
|
|
54
|
+
|
|
55
|
+
## Decision Outcome
|
|
56
|
+
|
|
57
|
+
**Flag interaction: Option A (hard error).** `--reconcile --only` exits with a mutually-exclusive error. `--only` renders a subset, but reconcile sweeps the shared `<region>/<cluster_type>/<color>` base root; the un-rendered siblings would be older than the marker and get deleted. Option C is unsafe (silent deletion of valid resources). Option B is plausible but adds per-entry scoping complexity for a workflow no one has asked for; SPP targeting is already served by `--spp`. We can revisit Option B if a real need appears.
|
|
58
|
+
|
|
59
|
+
**`--spp` scope derivation: Option 1 (sweep `SPP-PLACEHOLDER` plus requested targets).** `collect_reconcile_scopes` splits each entry's sweep root into base roots (non-SPP, swept with an `spp/` fence) and SPP roots. For an SPP root, `spp_reconcile_roots`:
|
|
60
|
+
|
|
61
|
+
* with no `--spp`, returns the `spp/SPP-PLACEHOLDER` root unchanged (only the placeholder subtree is swept; expanded instances are left alone);
|
|
62
|
+
* with `--spp X [Y…]`, returns the `spp/SPP-PLACEHOLDER` root **and** one root per requested target (`spp/X`, `spp/Y`), replacing the `SPP-PLACEHOLDER` segment for each target.
|
|
63
|
+
|
|
64
|
+
The original decision (Option 1a) excluded `SPP-PLACEHOLDER` under `--spp`. That was revised on 2026-07-06: because the pipeline re-renders `SPP-PLACEHOLDER` on every run before expanding it, its fresh files are always newer than the marker, so sweeping it is marker-safe and *necessary* — otherwise stale output from deleted/renamed templates accumulates in `SPP-PLACEHOLDER` indefinitely when a team only ever deploys with `--spp`. Unrequested SPP siblings are *not* re-rendered this run, so they remain excluded (sweeping them would delete valid current files). Each derived root is re-validated to stay within `rendered_directory`. Substitution reuses the existing `ResourceSet::SPP_PLACEHOLDER` constant, so reconcile does **not** depend on PR #14's `PlaceholderExpander`; the two constants are equal once #14 lands.
|
|
65
|
+
|
|
66
|
+
**SPP layout enforcement: only under `--reconcile` (A), both directions (1), validate shape (X).** The fence-and-substitute scoping above is only sound if SPP resources live at the canonical `<region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER/` layout. Nothing guaranteed this: SPP-ness is decided by the entry name containing `SPP-PLACEHOLDER`, and the deprecated `directory:` escape hatch can render anywhere while only printing a warning. So under `--reconcile`, `validate_spp_layout!` (run in `collect_reconcile_scopes`, before the marker and any writes) enforces:
|
|
67
|
+
|
|
68
|
+
* **SPP entry** — `output_directory` must equal the canonical `spp_base_root` (`<rendered>/<region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER`) or sit beneath it (`start_with?(base + File::SEPARATOR)`; the separator guards against `SPP-PLACEHOLDER-2`-style prefix collisions).
|
|
69
|
+
* **Non-SPP entry** — `output_directory` must not contain any `spp` path segment (reusing `within_spp_subtree?`).
|
|
70
|
+
|
|
71
|
+
A `directory:` override that still resolves to the canonical prefix is allowed; anything else raises `Reconciler::SppLayoutError` before rendering. Comparing against the fully-formatted canonical prefix (rather than counting path segments) stays correct even if a region value contains a slash. Because expansion derives its per-target subtrees from the already-validated `SPP-PLACEHOLDER` root, validating the pre-expansion path is sufficient — `--reconcile --spp` needs no extra check. Enforcing only under `--reconcile` (Option A) keeps plain renders, which never sweep, unaffected.
|
|
72
|
+
|
|
73
|
+
### Flag / layout interaction summary
|
|
74
|
+
|
|
75
|
+
| Flags | Reconcile sweep |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| `--reconcile` (no `--spp`) | base roots (with `spp/` fence) + `spp/SPP-PLACEHOLDER/` |
|
|
78
|
+
| `--reconcile --spp X [Y…]` | base roots (fenced) + `spp/SPP-PLACEHOLDER/` + one root per requested `spp/X/`, `spp/Y/`; unrequested SPPs excluded |
|
|
79
|
+
| `--reconcile --prune` | hard error |
|
|
80
|
+
| `--reconcile --only …` | hard error |
|
|
81
|
+
| SPP entry rendering outside `spp/SPP-PLACEHOLDER/` (under `--reconcile`) | `SppLayoutError` before any writes |
|
|
82
|
+
| non-SPP entry rendering under any `spp/` segment (under `--reconcile`) | `SppLayoutError` before any writes |
|
|
83
|
+
|
|
84
|
+
### Positive Consequences
|
|
85
|
+
|
|
86
|
+
* `--spp` deploys reconcile the targeted SPP subtree(s) **and** `SPP-PLACEHOLDER`, so stale template output no longer accumulates in the placeholder tree; unrequested SPPs are never swept.
|
|
87
|
+
* Reconcile is decoupled from expansion: it sweeps based on on-disk state and mtimes regardless of how files were produced, so it needs no `PlaceholderExpander` dependency.
|
|
88
|
+
* The fence-and-substitute scoping rests on an enforced layout invariant rather than a convention.
|
|
89
|
+
* The two destructive-combination footguns (`--prune`, `--only`) fail fast with clear messages.
|
|
90
|
+
|
|
91
|
+
### Negative Consequences
|
|
92
|
+
|
|
93
|
+
* `--reconcile --only` is unavailable; subset-targeted reconcile must go through `--spp`.
|
|
94
|
+
* Deleted-SPP cleanup for *unrequested* siblings, and placeholder-flip cleanup, remain manual `git rm` (no GC mode, no `--prune-old-spps`), as scoped in the ticket.
|
|
95
|
+
* A `directory:`-based SPP layout that reviewers previously tolerated now hard-errors under `--reconcile` and must move onto the canonical prefix.
|
|
96
|
+
|
|
97
|
+
## Implementation Notes
|
|
98
|
+
|
|
99
|
+
* `lib/kubernetes_template_rendering/cli.rb` — adds `--spp` / `--only` parsing (repeatable, de-duplicated), threads `spps:` into the renderer, and adds the `--reconcile --only` guard.
|
|
100
|
+
* `lib/kubernetes_template_rendering/cli_arguments.rb` — adds `:spps`, `:only`.
|
|
101
|
+
* `lib/kubernetes_template_rendering/resource_set.rb` — accepts `spps:`; `reconcile_scopes` carries `spp:` and the canonical `spp_base_root:` per scope.
|
|
102
|
+
* `lib/kubernetes_template_rendering/reconciler.rb` — adds `Reconciler::SppLayoutError`.
|
|
103
|
+
* `lib/kubernetes_template_rendering/template_directory_renderer.rb` — `collect_reconcile_scopes` calls `spp_reconcile_roots` (SPP-PLACEHOLDER + requested targets) and `validate_spp_layout!` (after the existing scope-escape pass).
|
|
104
|
+
|
|
105
|
+
### Merge ordering with PR #14
|
|
106
|
+
|
|
107
|
+
PR #14 landed on `main` first and OCTO-842 was merged on top of it. The `--spp` / `--only` flag plumbing, `CLIArguments` fields, and renderer signature were intentionally mirrored from PR #14, so the merge de-duplicated cleanly (identical hunks). Two pieces are owned by PR #14 rather than this change: the `--only` *filtering* of rendered entries (this ADR only adds the `--reconcile` + `--only` guard) and `PlaceholderExpander` (this change reuses the existing `ResourceSet::SPP_PLACEHOLDER` constant for sweep-root substitution and depends on the expander only at runtime). Since `--spp` expansion now runs during render (`PlaceholderExpander` preserves source mtimes), a `--spp` reconcile is a true end-to-end flow: expanded per-SPP files land after the marker and survive, while stale files in `SPP-PLACEHOLDER` and the requested SPP subtree are swept.
|
|
108
|
+
|
|
109
|
+
## Links
|
|
110
|
+
|
|
111
|
+
* Refines [ADR-0001](0001-strict-rendering-paths-for-stale-resource-deletion.md)
|
|
112
|
+
* Supersedes the standalone ADR-0003 draft (SPP layout guard), now folded into this record.
|
|
113
|
+
* Depends on [PR #14](https://github.com/Invoca/kubernetes_template_rendering/pull/14)
|
|
114
|
+
* Supersedes the implementation spec previously at `docs/superpowers/specs/2026-07-06-reconcile-spp-layout-guard-design.md` (removed; its content is folded into this ADR).
|
|
@@ -32,7 +32,16 @@ module KubernetesTemplateRendering
|
|
|
32
32
|
op.on("--region=REGION", "set the specific region to render") { args.region = _1 }
|
|
33
33
|
op.on("--color=COLOR", "set the specific color to render") { args.color = _1 }
|
|
34
34
|
op.on("--[no-]prune", "enable/disable pruning of untouched resources") { args.prune = _1 }
|
|
35
|
+
op.on("--[no-]reconcile", "bounded post-render sweep of stale resources") { args.reconcile = _1 }
|
|
35
36
|
op.on("--source-repo=SOURCE_REPO", "set the source repo for the rendered templates") { args.source_repo = _1 }
|
|
37
|
+
op.on("--spp=NAME", "Staging Partial Platform target name to expand SPP-PLACEHOLDER output for (repeatable)") do |name|
|
|
38
|
+
args.spps ||= []
|
|
39
|
+
args.spps << name
|
|
40
|
+
end
|
|
41
|
+
op.on("--only=NAME", "render only the definitions.yaml entry with this exact top-level key (repeatable)") do |name|
|
|
42
|
+
args.only ||= []
|
|
43
|
+
args.only << name
|
|
44
|
+
end
|
|
36
45
|
|
|
37
46
|
op.on("--variable-override=KEY:VALUE", "override a variable value set within definitions.yaml", Array) do |overrides|
|
|
38
47
|
args.variable_overrides ||= {} # Initialize as a Hash
|
|
@@ -50,12 +59,27 @@ module KubernetesTemplateRendering
|
|
|
50
59
|
|
|
51
60
|
parser.parse!(options)
|
|
52
61
|
args.template_directory = options.first
|
|
62
|
+
args.spps = (args.spps || []).uniq
|
|
63
|
+
args.only = (args.only || []).uniq
|
|
53
64
|
|
|
54
65
|
unless args.valid?
|
|
55
66
|
STDERR.puts(parser)
|
|
56
67
|
exit(1)
|
|
57
68
|
end
|
|
58
69
|
|
|
70
|
+
if args.reconcile? && args.prune?
|
|
71
|
+
STDERR.puts("--reconcile and --prune are mutually exclusive")
|
|
72
|
+
exit(1)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# --only renders a subset of entries, but reconcile sweeps the shared <region>/<type>/<color>
|
|
76
|
+
# base root; the un-rendered siblings would look stale and be deleted. SPP targeting uses --spp
|
|
77
|
+
# (which scopes reconcile to per-SPP subtrees), so --only + --reconcile is rejected outright.
|
|
78
|
+
if args.reconcile? && args.only.any?
|
|
79
|
+
STDERR.puts("--reconcile and --only are mutually exclusive")
|
|
80
|
+
exit(1)
|
|
81
|
+
end
|
|
82
|
+
|
|
59
83
|
[renderer_from_args(args), args]
|
|
60
84
|
end
|
|
61
85
|
|
|
@@ -69,7 +93,9 @@ module KubernetesTemplateRendering
|
|
|
69
93
|
region: args.region,
|
|
70
94
|
color: args.color,
|
|
71
95
|
variable_overrides: args.variable_overrides,
|
|
72
|
-
source_repo: args.source_repo
|
|
96
|
+
source_repo: args.source_repo,
|
|
97
|
+
spps: args.spps,
|
|
98
|
+
only: args.only
|
|
73
99
|
)
|
|
74
100
|
end
|
|
75
101
|
|
|
@@ -13,7 +13,10 @@ module KubernetesTemplateRendering
|
|
|
13
13
|
:color,
|
|
14
14
|
:variable_overrides,
|
|
15
15
|
:prune,
|
|
16
|
-
:source_repo
|
|
16
|
+
:source_repo,
|
|
17
|
+
:reconcile,
|
|
18
|
+
:spps,
|
|
19
|
+
:only
|
|
17
20
|
) do
|
|
18
21
|
def valid?
|
|
19
22
|
rendered_directory && template_directory
|
|
@@ -31,6 +34,10 @@ module KubernetesTemplateRendering
|
|
|
31
34
|
!!prune
|
|
32
35
|
end
|
|
33
36
|
|
|
37
|
+
def reconcile?
|
|
38
|
+
!!reconcile
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
def render_files?
|
|
35
42
|
true
|
|
36
43
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module KubernetesTemplateRendering
|
|
7
|
+
# Expands a rendered placeholder-bearing output tree into a per-target sibling tree by
|
|
8
|
+
# substituting the placeholder token in both file paths and file contents. Mirrors the
|
|
9
|
+
# semantics of invocaops_docker/tools/spp-transform/spp-transform.rb.
|
|
10
|
+
class PlaceholderExpander
|
|
11
|
+
class << self
|
|
12
|
+
def expand!(source_directory:, target_name:, placeholder_token:)
|
|
13
|
+
placeholder_suffix = placeholder_token.rpartition('-').last
|
|
14
|
+
target_suffix = target_name.rpartition('-').last
|
|
15
|
+
|
|
16
|
+
source_root = Pathname.new(source_directory)
|
|
17
|
+
dest_root = Pathname.new(source_directory.gsub(placeholder_token, target_name))
|
|
18
|
+
|
|
19
|
+
Pathname.glob(source_root + "**" + "*").each do |source_path|
|
|
20
|
+
next unless source_path.file?
|
|
21
|
+
|
|
22
|
+
relative = source_path.relative_path_from(source_root).to_s
|
|
23
|
+
relative_substituted = relative.gsub(placeholder_token, target_name)
|
|
24
|
+
dest_path = dest_root + relative_substituted
|
|
25
|
+
FileUtils.mkdir_p(dest_path.dirname)
|
|
26
|
+
|
|
27
|
+
mtime = source_path.mtime
|
|
28
|
+
contents = File.read(source_path)
|
|
29
|
+
contents = contents.gsub(placeholder_token, target_name)
|
|
30
|
+
contents = contents.gsub(placeholder_suffix, target_suffix)
|
|
31
|
+
File.write(dest_path, contents)
|
|
32
|
+
File.utime(mtime, mtime, dest_path)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "find"
|
|
5
|
+
require "tempfile"
|
|
6
|
+
require_relative "color"
|
|
7
|
+
|
|
8
|
+
module KubernetesTemplateRendering
|
|
9
|
+
# Bounded, marker-based reconcile sweep replacing the destructive per-entry `rm -rf` prune.
|
|
10
|
+
#
|
|
11
|
+
# Lifecycle (driven by TemplateDirectoryRenderer):
|
|
12
|
+
# 1. Construct (touches a marker file; its mtime is the cutoff) BEFORE any rendering/forking.
|
|
13
|
+
# 2. Render writes files (mtime > marker); PlaceholderExpander copies preserve a just-rendered
|
|
14
|
+
# source mtime (also > marker).
|
|
15
|
+
# 3. After all rendering, `sweep!` each scope root: delete files strictly older than the marker
|
|
16
|
+
# (leftovers from deleted/renamed entries), then remove now-empty directories.
|
|
17
|
+
#
|
|
18
|
+
# `spp/` subtrees are fenced out of the base sweep; per the ticket, deleted-SPP cleanup is a
|
|
19
|
+
# manual `git rm`, never an automatic sweep.
|
|
20
|
+
class Reconciler
|
|
21
|
+
class OutOfScopeError < StandardError; end
|
|
22
|
+
class SppLayoutError < StandardError; end
|
|
23
|
+
|
|
24
|
+
attr_reader :marker_mtime
|
|
25
|
+
|
|
26
|
+
def initialize(rendered_directory)
|
|
27
|
+
FileUtils.mkdir_p(rendered_directory)
|
|
28
|
+
@marker = Tempfile.new(".reconcile-marker", rendered_directory)
|
|
29
|
+
@marker.close
|
|
30
|
+
@marker_mtime = File.mtime(@marker.path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sweep!(root:, fences: [])
|
|
34
|
+
return unless File.directory?(root)
|
|
35
|
+
|
|
36
|
+
real_root = File.realpath(root)
|
|
37
|
+
fence_set = fences.select { |fence| File.exist?(fence) }.map { |fence| File.realpath(fence) }
|
|
38
|
+
|
|
39
|
+
delete_stale_files(root, real_root, fence_set)
|
|
40
|
+
remove_empty_dirs(root, fence_set)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def finish!
|
|
44
|
+
@marker.unlink
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Two-phase for all-or-nothing safety: validate the whole tree while collecting,
|
|
50
|
+
# then delete only after the walk succeeds. A scope error mid-walk deletes nothing.
|
|
51
|
+
def delete_stale_files(root, real_root, fence_set)
|
|
52
|
+
collect_stale_files(root, real_root, fence_set).each do |path|
|
|
53
|
+
puts "Reconcile: removing stale file #{Color.magenta(path)}"
|
|
54
|
+
File.delete(path)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def collect_stale_files(root, real_root, fence_set)
|
|
59
|
+
stale_paths = []
|
|
60
|
+
|
|
61
|
+
Find.find(root) do |path|
|
|
62
|
+
if File.directory?(path)
|
|
63
|
+
# Skip fenced subtrees (e.g. spp/) entirely; validate every other directory.
|
|
64
|
+
if fenced?(path, fence_set)
|
|
65
|
+
Find.prune
|
|
66
|
+
else
|
|
67
|
+
ensure_in_scope!(path, root, real_root)
|
|
68
|
+
end
|
|
69
|
+
next
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
next unless File.file?(path)
|
|
73
|
+
|
|
74
|
+
ensure_in_scope!(path, root, real_root)
|
|
75
|
+
stale_paths << path if stale?(path)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
stale_paths
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def stale?(path)
|
|
82
|
+
File.mtime(path) < @marker_mtime
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def remove_empty_dirs(root, fence_set)
|
|
86
|
+
Dir.glob(File.join(root, "**", "*/"))
|
|
87
|
+
.reject { |dir| fenced?(dir, fence_set) }
|
|
88
|
+
.sort
|
|
89
|
+
.reverse # deepest paths first so parents empty out after their children
|
|
90
|
+
.each do |dir|
|
|
91
|
+
next unless File.directory?(dir)
|
|
92
|
+
|
|
93
|
+
if Dir.empty?(dir)
|
|
94
|
+
puts "Reconcile: removing empty directory #{Color.magenta(dir)}"
|
|
95
|
+
Dir.rmdir(dir)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ensure_in_scope!(path, root, real_root)
|
|
101
|
+
real = File.realpath(path)
|
|
102
|
+
return if real == real_root || real.start_with?(real_root + File::SEPARATOR)
|
|
103
|
+
|
|
104
|
+
raise OutOfScopeError, "reconcile: #{path} resolves outside sweep root #{root}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def fenced?(path, fence_set)
|
|
108
|
+
return false if fence_set.empty?
|
|
109
|
+
|
|
110
|
+
resolved = File.realpath(path)
|
|
111
|
+
fence_set.any? { |fence| resolved == fence || resolved.start_with?(fence + File::SEPARATOR) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "resource"
|
|
4
4
|
require_relative "deploy_grouped_resource"
|
|
5
|
+
require_relative "placeholder_expander"
|
|
5
6
|
|
|
6
7
|
# This class points to the resources in a given template_directory for a given kubernetes_cluster_type like 'ops' or 'prod' or 'ci'.
|
|
7
8
|
# `config` contains the definitions found in `definitions_path`.
|
|
8
|
-
# The
|
|
9
|
+
# The output directory is resolved from the mutually-exclusive "directory" and "subdirectory" config:
|
|
10
|
+
# "directory" is a full pattern like "%{plain_region}/%{type}/%{color}/staging-ops".
|
|
11
|
+
# "subdirectory" is a literal final segment appended to the base path -> "%{plain_region}/%{type}/%{color}/<subdirectory>".
|
|
12
|
+
# when neither is given, output goes to the base path "%{plain_region}/%{type}/%{color}".
|
|
9
13
|
# The config "regions", "colors" are the sets of regions and colors to render for.
|
|
10
14
|
# It renders into `rendered_directory`.
|
|
11
15
|
module KubernetesTemplateRendering
|
|
@@ -13,14 +17,16 @@ module KubernetesTemplateRendering
|
|
|
13
17
|
attr_reader :variables, :output_directory, :deploy_group_config, :omitted_resources,
|
|
14
18
|
:template_directory, :target_output_directory, :regions, :colors,
|
|
15
19
|
:definitions_path, :kubernetes_cluster_type, :variable_overrides,
|
|
16
|
-
:source_repo
|
|
20
|
+
:source_repo, :spps
|
|
17
21
|
|
|
18
|
-
def initialize(config:, template_directory:, rendered_directory:, definitions_path:, kubernetes_cluster_type:,
|
|
22
|
+
def initialize(config:, template_directory:, rendered_directory:, definitions_path:, kubernetes_cluster_type:,
|
|
23
|
+
spp: false, spps: [], variable_overrides: {}, source_repo: nil)
|
|
24
|
+
@spp = spp
|
|
19
25
|
@variables = config["variables"] || {}
|
|
20
26
|
@deploy_group_config = config["deploy_groups"]
|
|
21
27
|
@omitted_resources = config["omitted_resources"]
|
|
22
28
|
@template_directory = template_directory
|
|
23
|
-
@target_output_directory = config["directory"]
|
|
29
|
+
@target_output_directory = resolve_target_output_directory(config["directory"], config["subdirectory"])
|
|
24
30
|
@regions = config["regions"] || []
|
|
25
31
|
@colors = config["colors"] || []
|
|
26
32
|
@rendered_directory = rendered_directory
|
|
@@ -29,6 +35,7 @@ module KubernetesTemplateRendering
|
|
|
29
35
|
@variable_overrides = variable_overrides
|
|
30
36
|
@source_repo = source_repo
|
|
31
37
|
@resources = {}
|
|
38
|
+
@spps = Array(spps)
|
|
32
39
|
|
|
33
40
|
if @kubernetes_cluster_type != "kube-platform"
|
|
34
41
|
@target_output_directory.include?("%{plain_region}") or raise "#{@template_directory}: target_output_directory #{@target_output_directory} needs %{plain_region}"
|
|
@@ -65,6 +72,28 @@ module KubernetesTemplateRendering
|
|
|
65
72
|
end
|
|
66
73
|
end
|
|
67
74
|
|
|
75
|
+
# Bounded reconcile scope roots for this entry, one per region × color.
|
|
76
|
+
#
|
|
77
|
+
# The sweep root is the parent directory of the entry's rendered `output_directory`, derived
|
|
78
|
+
# from the actual `directory:` pattern rather than assuming a fixed layout. For a
|
|
79
|
+
# `.../<service>` pattern that parent is the `<region>/<cluster_type>/<color>` base; for a
|
|
80
|
+
# `<region>/<service>` pattern it is `<region>`. The parent is shared with sibling entries, so
|
|
81
|
+
# leftovers from a deleted/renamed entry under it are swept. The renderer validates each
|
|
82
|
+
# `base_root` stays within `rendered_directory` (out-of-prefix, full or relative, is a hard error).
|
|
83
|
+
#
|
|
84
|
+
# Each scope also carries `spp:` (whether this entry is an SPP definition) and `spp_base_root:`
|
|
85
|
+
# (the canonical `<region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER` prefix for that region ×
|
|
86
|
+
# color) so the renderer can enforce the SPP layout guard before rendering.
|
|
87
|
+
def reconcile_scopes
|
|
88
|
+
@regions.flat_map do |plain_region|
|
|
89
|
+
@colors.map do |c|
|
|
90
|
+
output_directory = File.join(@rendered_directory, format(@target_output_directory, plain_region: plain_region, color: c, type: @kubernetes_cluster_type))
|
|
91
|
+
spp_base_root = File.join(@rendered_directory, format(SPP_BASE_OUTPUT_DIRECTORY, plain_region: plain_region, color: c, type: @kubernetes_cluster_type))
|
|
92
|
+
{ base_root: File.dirname(output_directory), output_directory: output_directory, spp: @spp, spp_base_root: spp_base_root }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
68
97
|
def render_create_directory(args, output_directory)
|
|
69
98
|
prune_directory(output_directory) if args.prune?
|
|
70
99
|
create_directory(output_directory)
|
|
@@ -80,11 +109,53 @@ module KubernetesTemplateRendering
|
|
|
80
109
|
resources(output_directory).each do |resource|
|
|
81
110
|
resource.render(args)
|
|
82
111
|
end
|
|
112
|
+
expand_placeholders(args, output_directory)
|
|
83
113
|
puts
|
|
84
114
|
end
|
|
85
115
|
|
|
86
116
|
private
|
|
87
117
|
|
|
118
|
+
# The output-directory pattern is resolved from the mutually-exclusive
|
|
119
|
+
# `directory:` and `subdirectory:` config fields:
|
|
120
|
+
# directory only -> the directory pattern, verbatim
|
|
121
|
+
# subdirectory only -> "%{plain_region}/%{type}/%{color}/<subdirectory>"
|
|
122
|
+
# neither -> base path; "%{plain_region}/%{type}/%{color}", or the SPP base
|
|
123
|
+
# "%{plain_region}/%{type}/%{color}/spp/SPP-PLACEHOLDER" for SPP definitions
|
|
124
|
+
# both -> ArgumentError
|
|
125
|
+
SPP_PLACEHOLDER = "SPP-PLACEHOLDER"
|
|
126
|
+
BASE_OUTPUT_DIRECTORY = File.join("%{plain_region}", "%{type}", "%{color}")
|
|
127
|
+
# SPP definitions render under an extra spp/SPP-PLACEHOLDER segment so each SPP
|
|
128
|
+
# instance has a distinct, bounded path and the literal token survives for
|
|
129
|
+
# downstream per-instance substitution.
|
|
130
|
+
SPP_BASE_OUTPUT_DIRECTORY = File.join(BASE_OUTPUT_DIRECTORY, "spp", SPP_PLACEHOLDER)
|
|
131
|
+
|
|
132
|
+
def resolve_target_output_directory(directory, subdirectory)
|
|
133
|
+
if directory && subdirectory
|
|
134
|
+
raise ArgumentError, "specify only one of 'directory:' or 'subdirectory:' in #{({ 'directory' => directory, 'subdirectory' => subdirectory }).inspect}"
|
|
135
|
+
elsif directory
|
|
136
|
+
warn_directory_deprecated(directory)
|
|
137
|
+
directory
|
|
138
|
+
elsif subdirectory
|
|
139
|
+
File.join(base_output_directory, subdirectory)
|
|
140
|
+
else
|
|
141
|
+
base_output_directory
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# SPP definitions render under SPP_BASE_OUTPUT_DIRECTORY; everything else uses the standard base.
|
|
146
|
+
def base_output_directory
|
|
147
|
+
@spp ? SPP_BASE_OUTPUT_DIRECTORY : BASE_OUTPUT_DIRECTORY
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# `directory:` is a deprecated legacy escape hatch. It is the only way to produce a
|
|
151
|
+
# path that is not derived from the standard "%{plain_region}/%{type}/%{color}" layout,
|
|
152
|
+
# which is unsafe for --reconcile stale-resource deletion (see ADR-0001). Warn on any use.
|
|
153
|
+
def warn_directory_deprecated(directory)
|
|
154
|
+
puts Color.brown("WARNING: #{@template_directory}: `directory:` is deprecated. " \
|
|
155
|
+
"Remove it to render into the standard #{base_output_directory} layout, " \
|
|
156
|
+
"or use `subdirectory:` instead. (got `directory: #{directory}`)")
|
|
157
|
+
end
|
|
158
|
+
|
|
88
159
|
CLOUD_REGION_TO_PROVIDER_AND_DATACENTER = {
|
|
89
160
|
# Note: The names below should match RegionDiscovery from process_settings-production.
|
|
90
161
|
# https://github.com/Invoca/process_settings-production/blob/main/settings/region_discovery/production_regions.yml
|
|
@@ -182,6 +253,27 @@ module KubernetesTemplateRendering
|
|
|
182
253
|
end
|
|
183
254
|
end
|
|
184
255
|
|
|
256
|
+
# Two orthogonal gates:
|
|
257
|
+
# @spps.empty? - caller didn't pass --spp NAME; nothing to fan out to
|
|
258
|
+
# !@spp - entry name is not SPP-shaped, so output_directory does not contain
|
|
259
|
+
# SPP_PLACEHOLDER; gsub-based expansion would be a no-op that hits
|
|
260
|
+
# --prune as a data-loss footgun (per_spp_dir collides with source).
|
|
261
|
+
def expand_placeholders(args, output_directory)
|
|
262
|
+
return if @spps.empty?
|
|
263
|
+
return unless @spp
|
|
264
|
+
|
|
265
|
+
@spps.each do |spp_name|
|
|
266
|
+
per_spp_dir = output_directory.gsub(SPP_PLACEHOLDER, spp_name)
|
|
267
|
+
prune_directory(per_spp_dir) if args.prune?
|
|
268
|
+
puts "Expanding #{SPP_PLACEHOLDER} to #{Color.magenta(spp_name)}"
|
|
269
|
+
PlaceholderExpander.expand!(
|
|
270
|
+
source_directory: output_directory,
|
|
271
|
+
target_name: spp_name,
|
|
272
|
+
placeholder_token: SPP_PLACEHOLDER
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
185
277
|
def resources(output_directory)
|
|
186
278
|
@resources[output_directory] ||= standard_resources(output_directory) + grouped_resources(output_directory)
|
|
187
279
|
end
|
|
@@ -25,11 +25,11 @@ module KubernetesTemplateRendering
|
|
|
25
25
|
|
|
26
26
|
def with_auto_generated_yaml_comment(yaml_string)
|
|
27
27
|
comment = <<~EOS
|
|
28
|
-
# WARNING: DO
|
|
28
|
+
# WARNING: DO NOT EDIT THIS FILE!
|
|
29
29
|
# Any changes made here will be lost.
|
|
30
30
|
# This file is autogenerated from #{full_template_path}
|
|
31
31
|
EOS
|
|
32
|
-
comment +
|
|
32
|
+
comment + yaml_string
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
def full_template_path
|
|
@@ -40,14 +40,6 @@ module KubernetesTemplateRendering
|
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def variable_override_comment
|
|
44
|
-
if variable_overrides&.any?
|
|
45
|
-
<<~EOS
|
|
46
|
-
# Variable overrides used: #{variable_overrides.to_json}
|
|
47
|
-
EOS
|
|
48
|
-
end || ""
|
|
49
|
-
end
|
|
50
|
-
|
|
51
43
|
class << self
|
|
52
44
|
# @param [String] template_path: file path to template file that needs to be rendered.
|
|
53
45
|
# @param [Hash]: variables that will be used in the template file to generate distict files.
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
require 'active_support'
|
|
4
4
|
require 'active_support/core_ext' # for deep_merge
|
|
5
5
|
require 'invoca/utils'
|
|
6
|
+
require 'pathname'
|
|
6
7
|
require 'yaml'
|
|
7
8
|
require_relative 'resource_set'
|
|
9
|
+
require_relative 'reconciler'
|
|
8
10
|
require 'set'
|
|
9
11
|
|
|
10
12
|
# This class points to a collection of template directories to render, and a rendered_directory to render into.
|
|
@@ -12,10 +14,13 @@ require 'set'
|
|
|
12
14
|
module KubernetesTemplateRendering
|
|
13
15
|
class TemplateDirectoryRenderer
|
|
14
16
|
DEFINITIONS_FILENAME = "definitions.yaml"
|
|
17
|
+
SPP_FENCE_DIRNAME = "spp"
|
|
15
18
|
|
|
16
|
-
attr_reader :directories, :omitted_names, :rendered_directory, :cluster_type, :region, :color,
|
|
19
|
+
attr_reader :directories, :omitted_names, :rendered_directory, :cluster_type, :region, :color,
|
|
20
|
+
:variable_overrides, :source_repo, :spps, :only
|
|
17
21
|
|
|
18
|
-
def initialize(directories:, rendered_directory:, omitted_names: [], cluster_type: nil, region: nil, color: nil,
|
|
22
|
+
def initialize(directories:, rendered_directory:, omitted_names: [], cluster_type: nil, region: nil, color: nil,
|
|
23
|
+
variable_overrides: nil, source_repo: nil, spps: [], only: [])
|
|
19
24
|
@directories = directories_with_definitions(Array(directories))
|
|
20
25
|
@omitted_names = Array(omitted_names)
|
|
21
26
|
@rendered_directory = rendered_directory
|
|
@@ -24,9 +29,16 @@ module KubernetesTemplateRendering
|
|
|
24
29
|
@color = color
|
|
25
30
|
@variable_overrides = variable_overrides || {}
|
|
26
31
|
@source_repo = source_repo
|
|
32
|
+
@spps = Array(spps)
|
|
33
|
+
@only = Array(only)
|
|
27
34
|
end
|
|
28
35
|
|
|
29
36
|
def render(args)
|
|
37
|
+
if args.reconcile?
|
|
38
|
+
sweep_scopes = collect_reconcile_scopes # validates out-of-prefix before any writes
|
|
39
|
+
reconciler = Reconciler.new(@rendered_directory) # marker captured before rendering/forking
|
|
40
|
+
end
|
|
41
|
+
|
|
30
42
|
child_pids = []
|
|
31
43
|
failed_processes = [] # [[pid, status]]
|
|
32
44
|
|
|
@@ -58,10 +70,103 @@ module KubernetesTemplateRendering
|
|
|
58
70
|
raise "Child process completed with non-zero status: #{failed_processes.inspect}"
|
|
59
71
|
end
|
|
60
72
|
end
|
|
73
|
+
|
|
74
|
+
reconcile_sweep(reconciler, sweep_scopes) if args.reconcile?
|
|
61
75
|
end
|
|
62
76
|
|
|
63
77
|
private
|
|
64
78
|
|
|
79
|
+
# Collects sweep roots across all rendered resource sets and validates that each stays within
|
|
80
|
+
# rendered_directory (out-of-prefix, full or relative `..`, is a hard error).
|
|
81
|
+
#
|
|
82
|
+
# Returns two categories:
|
|
83
|
+
# base_roots — non-SPP entries' shared parent (e.g. <region>/<cluster_type>/<color>/);
|
|
84
|
+
# swept with an spp/ fence so sibling SPP directories are never touched.
|
|
85
|
+
# spp_roots — the specific SPP directory/directories that were rendered (e.g. spp/<spp-name>/);
|
|
86
|
+
# swept without a fence since we are already inside exactly one SPP directory.
|
|
87
|
+
# Without --spp, only the SPP-PLACEHOLDER root is swept. With --spp, the sweep
|
|
88
|
+
# covers SPP-PLACEHOLDER (always re-rendered) plus each requested target; unrequested
|
|
89
|
+
# siblings are left intact (their deleted-SPP cleanup stays a manual git rm).
|
|
90
|
+
def collect_reconcile_scopes
|
|
91
|
+
scopes = resource_sets.values.flatten.flat_map(&:reconcile_scopes)
|
|
92
|
+
scopes.each do |scope|
|
|
93
|
+
validate_within_scope!(scope[:base_root], @rendered_directory)
|
|
94
|
+
validate_spp_layout!(scope)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
base_roots = []
|
|
98
|
+
spp_roots = []
|
|
99
|
+
scopes.each do |scope|
|
|
100
|
+
if within_spp_subtree?(scope[:base_root])
|
|
101
|
+
spp_roots.concat(spp_reconcile_roots(scope))
|
|
102
|
+
else
|
|
103
|
+
base_roots << scope[:base_root]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
spp_roots.each { |r| validate_within_scope!(r, @rendered_directory) }
|
|
107
|
+
{ base_roots: base_roots.uniq, spp_roots: spp_roots.uniq }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Raises unless `path` resolves within `scope_root` (full-path or relative `..` escape).
|
|
111
|
+
# Lexical check (expand_path, not realpath) so it validates planned roots before they exist;
|
|
112
|
+
# Reconciler applies a separate realpath-based guard against symlink escapes during the sweep.
|
|
113
|
+
def validate_within_scope!(path, scope_root)
|
|
114
|
+
resolved = File.expand_path(path)
|
|
115
|
+
root = File.expand_path(scope_root)
|
|
116
|
+
unless resolved == root || resolved.start_with?(root + File::SEPARATOR)
|
|
117
|
+
raise Reconciler::OutOfScopeError, "reconcile: path #{resolved} resolves outside scope prefix #{root}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Under --reconcile, SPP entries must render beneath the canonical
|
|
122
|
+
# <region>/<cluster_type>/<color>/spp/SPP-PLACEHOLDER/ prefix, and non-SPP entries must never
|
|
123
|
+
# render beneath any spp/ segment. A directory:-overridden path that still resolves to the
|
|
124
|
+
# canonical SPP prefix is allowed; anything else is a hard error before any writes.
|
|
125
|
+
def validate_spp_layout!(scope)
|
|
126
|
+
output = File.expand_path(scope[:output_directory])
|
|
127
|
+
if scope[:spp]
|
|
128
|
+
base = File.expand_path(scope[:spp_base_root])
|
|
129
|
+
unless output == base || output.start_with?(base + File::SEPARATOR)
|
|
130
|
+
raise Reconciler::SppLayoutError,
|
|
131
|
+
"reconcile: SPP entry renders to #{output}, outside the required SPP prefix #{base}"
|
|
132
|
+
end
|
|
133
|
+
elsif within_spp_subtree?(scope[:output_directory])
|
|
134
|
+
raise Reconciler::SppLayoutError,
|
|
135
|
+
"reconcile: non-SPP entry renders under an spp/ segment (#{output}); only SPP entries may render beneath spp/"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Expands an SPP sweep root into the concrete roots to sweep for this run.
|
|
140
|
+
# Without --spp, the placeholder root (spp/SPP-PLACEHOLDER) is swept as-is. With --spp, the sweep
|
|
141
|
+
# covers the placeholder root *and* each requested target (spp/staging-qa02a, ...): SPP-PLACEHOLDER
|
|
142
|
+
# is always re-rendered (it is the expansion source) and each target is freshly expanded from it,
|
|
143
|
+
# so both are marker-safe. Unrequested SPP siblings are not re-rendered this run and stay excluded.
|
|
144
|
+
def spp_reconcile_roots(scope)
|
|
145
|
+
root = spp_sweep_root(scope)
|
|
146
|
+
return [root] if @spps.empty?
|
|
147
|
+
return [root] unless root.include?(ResourceSet::SPP_PLACEHOLDER)
|
|
148
|
+
|
|
149
|
+
[root] + @spps.map { |spp_name| root.gsub(ResourceSet::SPP_PLACEHOLDER, spp_name) }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def within_spp_subtree?(root)
|
|
153
|
+
Pathname.new(root).relative_path_from(Pathname.new(@rendered_directory)).each_filename.include?(SPP_FENCE_DIRNAME)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# When the directory pattern has no service subdirectory (e.g. `.../spp/SPP-PLACEHOLDER`),
|
|
157
|
+
# base_root lands at the `spp/` level itself — sweeping that would touch all SPP siblings.
|
|
158
|
+
# Use output_directory (= `spp/<spp-name>`) in that case instead.
|
|
159
|
+
def spp_sweep_root(scope)
|
|
160
|
+
File.basename(scope[:base_root]) == SPP_FENCE_DIRNAME ? scope[:output_directory] : scope[:base_root]
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def reconcile_sweep(reconciler, scopes)
|
|
164
|
+
scopes[:base_roots].each { |r| reconciler.sweep!(root: r, fences: [File.join(r, SPP_FENCE_DIRNAME)]) }
|
|
165
|
+
scopes[:spp_roots].each { |r| reconciler.sweep!(root: r) }
|
|
166
|
+
ensure
|
|
167
|
+
reconciler.finish!
|
|
168
|
+
end
|
|
169
|
+
|
|
65
170
|
def read_definitions(path)
|
|
66
171
|
File.read(path)
|
|
67
172
|
end
|
|
@@ -106,26 +211,48 @@ module KubernetesTemplateRendering
|
|
|
106
211
|
end
|
|
107
212
|
|
|
108
213
|
def resource_sets
|
|
109
|
-
@resource_sets ||=
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
kubernetes_cluster_type
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
214
|
+
@resource_sets ||= begin
|
|
215
|
+
matched_only = Set.new
|
|
216
|
+
all_keys = Set.new
|
|
217
|
+
sets = @directories.each_with_object({}) do |dir, hash|
|
|
218
|
+
definitions_path = definitions_path_for_dir(dir)
|
|
219
|
+
config = load_config(definitions_path)
|
|
220
|
+
|
|
221
|
+
config.each_key { |name| all_keys << name }
|
|
222
|
+
|
|
223
|
+
config.map do |name, config|
|
|
224
|
+
next if omitted_names.include?(name)
|
|
225
|
+
next if @only.any? && !@only.include?(name)
|
|
226
|
+
matched_only << name if @only.include?(name)
|
|
227
|
+
|
|
228
|
+
kubernetes_cluster_type = name.sub(ResourceSet::SPP_PLACEHOLDER, 'staging').sub(/\..*/, '') # prod.gcp => prod
|
|
229
|
+
spp = name.include?(ResourceSet::SPP_PLACEHOLDER)
|
|
230
|
+
|
|
231
|
+
hash[name] ||= []
|
|
232
|
+
hash[name] << ResourceSet.new(
|
|
233
|
+
config: config,
|
|
234
|
+
template_directory: dir,
|
|
235
|
+
rendered_directory: @rendered_directory,
|
|
236
|
+
kubernetes_cluster_type: kubernetes_cluster_type,
|
|
237
|
+
spp: spp,
|
|
238
|
+
spps: @spps,
|
|
239
|
+
definitions_path: definitions_path,
|
|
240
|
+
variable_overrides: @variable_overrides,
|
|
241
|
+
source_repo: @source_repo
|
|
242
|
+
)
|
|
243
|
+
end
|
|
128
244
|
end
|
|
245
|
+
|
|
246
|
+
if @only.any?
|
|
247
|
+
unmatched = @only - matched_only.to_a
|
|
248
|
+
if unmatched.any?
|
|
249
|
+
raise ArgumentError,
|
|
250
|
+
"--only values not found in any definitions.yaml entry: #{unmatched.inspect}. " \
|
|
251
|
+
"Valid keys: #{all_keys.to_a.sort.inspect}"
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
sets
|
|
129
256
|
end
|
|
130
257
|
end
|
|
131
258
|
|
|
@@ -173,7 +300,7 @@ module KubernetesTemplateRendering
|
|
|
173
300
|
end
|
|
174
301
|
|
|
175
302
|
expand_config(config).each_with_object({}) do |(name, data), hash|
|
|
176
|
-
if !cluster_type || cluster_type == name.sub(
|
|
303
|
+
if !cluster_type || cluster_type == name.sub(ResourceSet::SPP_PLACEHOLDER, 'staging').sub(/\..*/, '') # prod.gcp => prod
|
|
177
304
|
cluster_type_config = OpenStruct.new(data)
|
|
178
305
|
|
|
179
306
|
cluster_type_config.regions = cluster_type_config.regions & [region] if region
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kubernetes_template_rendering
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0.prerelease.tstarck.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Octothorpe
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-07-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -52,6 +52,20 @@ dependencies:
|
|
|
52
52
|
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: ostruct
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
55
69
|
description: Tool for rendering ERB and Jsonnet templates
|
|
56
70
|
email:
|
|
57
71
|
- octothorpe@invoca.com
|
|
@@ -71,6 +85,8 @@ files:
|
|
|
71
85
|
- docs/CHANGELOG.md
|
|
72
86
|
- docs/README.md
|
|
73
87
|
- docs/adrs/0000-template.md
|
|
88
|
+
- docs/adrs/0001-strict-rendering-paths-for-stale-resource-deletion.md
|
|
89
|
+
- docs/adrs/0002-spp-aware-reconcile-scopes-and-only-guard.md
|
|
74
90
|
- exe/render_templates
|
|
75
91
|
- lib/kubernetes_template_rendering.rb
|
|
76
92
|
- lib/kubernetes_template_rendering/cli.rb
|
|
@@ -79,6 +95,8 @@ files:
|
|
|
79
95
|
- lib/kubernetes_template_rendering/deploy_grouped_resource.rb
|
|
80
96
|
- lib/kubernetes_template_rendering/erb_template.rb
|
|
81
97
|
- lib/kubernetes_template_rendering/jsonnet_template.rb
|
|
98
|
+
- lib/kubernetes_template_rendering/placeholder_expander.rb
|
|
99
|
+
- lib/kubernetes_template_rendering/reconciler.rb
|
|
82
100
|
- lib/kubernetes_template_rendering/resource.rb
|
|
83
101
|
- lib/kubernetes_template_rendering/resource_set.rb
|
|
84
102
|
- lib/kubernetes_template_rendering/template.rb
|