matsuri 0.1.0 → 0.3.0

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.
Files changed (157) hide show
  1. checksums.yaml +5 -5
  2. data/.envrc +1 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +9 -0
  5. data/AGENTS.md +18 -0
  6. data/CHANGELOG.md +642 -0
  7. data/Gemfile +2 -0
  8. data/README.md +86 -8
  9. data/Rakefile +20 -0
  10. data/bin/console +1 -0
  11. data/bin/matsuri +6 -0
  12. data/bin/setup +2 -0
  13. data/docs/acceptors.md +181 -0
  14. data/docs/boot.md +101 -0
  15. data/docs/design/acceptors.md +157 -0
  16. data/docs/design/boot.md +131 -0
  17. data/docs/design/ssa.md +142 -0
  18. data/docs/design/stacks.md +153 -0
  19. data/docs/diff.md +85 -0
  20. data/docs/known-issues.md +19 -0
  21. data/docs/local-dev.md +173 -0
  22. data/docs/presenters.md +317 -0
  23. data/docs/resources.md +121 -0
  24. data/docs/setup.md +97 -0
  25. data/docs/ssa.md +149 -0
  26. data/docs/stacks.md +146 -0
  27. data/flake.lock +61 -0
  28. data/flake.nix +35 -0
  29. data/lib/matsuri/acceptor.rb +33 -0
  30. data/lib/matsuri/acceptors/image_tag.rb +15 -0
  31. data/lib/matsuri/cmd.rb +46 -0
  32. data/lib/matsuri/cmds/apply.rb +78 -0
  33. data/lib/matsuri/cmds/cli.rb +147 -0
  34. data/lib/matsuri/cmds/create.rb +87 -0
  35. data/lib/matsuri/cmds/delete.rb +86 -0
  36. data/lib/matsuri/cmds/diff.rb +105 -0
  37. data/lib/matsuri/cmds/generate.rb +24 -0
  38. data/lib/matsuri/cmds/kubectl.rb +16 -0
  39. data/lib/matsuri/cmds/recreate.rb +65 -0
  40. data/lib/matsuri/cmds/scale.rb +37 -0
  41. data/lib/matsuri/cmds/show.rb +123 -0
  42. data/lib/matsuri/cmds/status.rb +67 -0
  43. data/lib/matsuri/concerns/awaiting.rb +24 -0
  44. data/lib/matsuri/concerns/diffable.rb +135 -0
  45. data/lib/matsuri/concerns/export_manifest.rb +56 -0
  46. data/lib/matsuri/concerns/import_manifest.rb +64 -0
  47. data/lib/matsuri/concerns/pod_template.rb +37 -0
  48. data/lib/matsuri/concerns/pods/spread_pods.rb +47 -0
  49. data/lib/matsuri/concerns/pods/spread_topology.rb +63 -0
  50. data/lib/matsuri/concerns/registry_helpers.rb +59 -0
  51. data/lib/matsuri/config.rb +101 -0
  52. data/lib/matsuri/core_ext.rb +107 -0
  53. data/lib/matsuri/dsl/cluster/aggregated_cluster_role.rb +75 -0
  54. data/lib/matsuri/dsl/cluster/binding.rb +118 -0
  55. data/lib/matsuri/dsl/cluster/cluster_role.rb +41 -0
  56. data/lib/matsuri/dsl/cluster/role.rb +49 -0
  57. data/lib/matsuri/dsl/cluster/scope.rb +245 -0
  58. data/lib/matsuri/dsl/cluster/service_account.rb +76 -0
  59. data/lib/matsuri/dsl/concerns/default_binding.rb +34 -0
  60. data/lib/matsuri/dsl/concerns/manifest_set.rb +21 -0
  61. data/lib/matsuri/dsl/concerns/metadata.rb +56 -0
  62. data/lib/matsuri/dsl/concerns/rbac_rules.rb +39 -0
  63. data/lib/matsuri/initializer.rb +41 -0
  64. data/lib/matsuri/kubernetes/base.rb +107 -0
  65. data/lib/matsuri/kubernetes/config_map.rb +100 -0
  66. data/lib/matsuri/kubernetes/cron_job.rb +115 -0
  67. data/lib/matsuri/kubernetes/daemon_set.rb +62 -0
  68. data/lib/matsuri/kubernetes/deployment.rb +65 -0
  69. data/lib/matsuri/kubernetes/endpoints.rb +57 -0
  70. data/lib/matsuri/kubernetes/horizontal_pod_autoscaler.rb +116 -0
  71. data/lib/matsuri/kubernetes/imported_manifest.rb +61 -0
  72. data/lib/matsuri/kubernetes/ingress.rb +66 -0
  73. data/lib/matsuri/kubernetes/persistent_volume.rb +56 -0
  74. data/lib/matsuri/kubernetes/persistent_volume_claim.rb +41 -0
  75. data/lib/matsuri/kubernetes/pod.rb +355 -0
  76. data/lib/matsuri/kubernetes/replica_set.rb +46 -0
  77. data/lib/matsuri/kubernetes/secret.rb +86 -0
  78. data/lib/matsuri/kubernetes/service.rb +49 -0
  79. data/lib/matsuri/kubernetes/stateful_set.rb +75 -0
  80. data/lib/matsuri/kubernetes/storage_class.rb +45 -0
  81. data/lib/matsuri/platform.rb +38 -0
  82. data/lib/matsuri/presenter.rb +47 -0
  83. data/lib/matsuri/presenters/helpers.rb +39 -0
  84. data/lib/matsuri/presenters/prune_empty.rb +59 -0
  85. data/lib/matsuri/presenters/seed_image.rb +28 -0
  86. data/lib/matsuri/presenters/seed_replicas.rb +25 -0
  87. data/lib/matsuri/presenters/seed_resources.rb +29 -0
  88. data/lib/matsuri/registry.rb +267 -0
  89. data/lib/matsuri/shell_out.rb +119 -0
  90. data/lib/matsuri/stack.rb +276 -0
  91. data/lib/matsuri/task.rb +20 -0
  92. data/lib/matsuri/tasks/cluster.rb +79 -0
  93. data/lib/matsuri/tasks/docker.rb +25 -0
  94. data/lib/matsuri/tasks/hash_diff.rb +61 -0
  95. data/lib/matsuri/tasks/kubectl.rb +21 -0
  96. data/lib/matsuri/tasks/kubectl_ops/apply.rb +24 -0
  97. data/lib/matsuri/tasks/kubectl_ops/create.rb +27 -0
  98. data/lib/matsuri/tasks/kubectl_ops/delete.rb +21 -0
  99. data/lib/matsuri/tasks/kubectl_ops/diff.rb +47 -0
  100. data/lib/matsuri/tasks/kubectl_ops/get.rb +36 -0
  101. data/lib/matsuri/tasks/kubectl_ops/recreate.rb +35 -0
  102. data/lib/matsuri/tasks/kubectl_ops/replace.rb +29 -0
  103. data/lib/matsuri/tasks/kubectl_ops/rollout_status.rb +22 -0
  104. data/lib/matsuri/tasks/kubectl_ops/scale.rb +22 -0
  105. data/lib/matsuri/tasks/kubectl_ops/server_side_apply.rb +32 -0
  106. data/lib/matsuri/tasks/kubectl_ops/status.rb +20 -0
  107. data/lib/matsuri/tasks/kubectl_ops.rb +218 -0
  108. data/lib/matsuri/tasks/pod.rb +56 -0
  109. data/lib/matsuri/tasks/stacks/seed.rb +93 -0
  110. data/lib/matsuri/tasks/stacks.rb +31 -0
  111. data/lib/matsuri/transform/algebra.rb +52 -0
  112. data/lib/matsuri/transform/callable.rb +54 -0
  113. data/lib/matsuri/transform/combinators.rb +61 -0
  114. data/lib/matsuri/transform/env.rb +43 -0
  115. data/lib/matsuri/transform/kubernetes.rb +89 -0
  116. data/lib/matsuri/transform.rb +20 -0
  117. data/lib/matsuri/version.rb +3 -1
  118. data/lib/matsuri.rb +158 -2
  119. data/matsuri.gemspec +20 -7
  120. data/tests/bin/minikube-start.sh +27 -0
  121. data/tests/bin/probe-save-config.sh +46 -0
  122. data/tests/bin/probe-ssa.sh +206 -0
  123. data/tests/example/.gitignore +1 -0
  124. data/tests/example/Gemfile +7 -0
  125. data/tests/example/README.md +91 -0
  126. data/tests/example/bin/matsuri +14 -0
  127. data/tests/example/config/initializer.rb +12 -0
  128. data/tests/example/config/matsuri.rb +9 -0
  129. data/tests/example/config/platform.rb +39 -0
  130. data/tests/example/lib/acme/nodes/dedicated.rb +26 -0
  131. data/tests/example/lib/acme/nodes/preemptible.rb +17 -0
  132. data/tests/example/lib/acme/nodes/required_cores.rb +26 -0
  133. data/tests/example/lib/acme/nodes/resource_class.rb +26 -0
  134. data/tests/example/lib/acme/pods/cacerts.rb +15 -0
  135. data/tests/example/lib/acme/pods/healthcheck.rb +36 -0
  136. data/tests/example/lib/acme.rb +26 -0
  137. data/tests/example/platform/config_maps/billing-settings.rb +8 -0
  138. data/tests/example/platform/config_maps/cacerts.rb +9 -0
  139. data/tests/example/platform/deployments/billing-web.rb +5 -0
  140. data/tests/example/platform/deployments/checkout-web.rb +6 -0
  141. data/tests/example/platform/namespaces/billing-dev.yaml +7 -0
  142. data/tests/example/platform/namespaces/checkout-dev.yaml +7 -0
  143. data/tests/example/platform/pods/billing.rb +31 -0
  144. data/tests/example/platform/pods/checkout.rb +27 -0
  145. data/tests/example/platform/services/billing-web.rb +6 -0
  146. data/tests/example/platform/services/checkout-web.rb +4 -0
  147. data/tests/example/platform/stacks/billing.rb +8 -0
  148. data/tests/example/platform/stacks/checkout.rb +19 -0
  149. data/tests/k8s/save-config/deployment-declared.yaml +19 -0
  150. data/tests/k8s/save-config/deployment-seeded.yaml +20 -0
  151. data/tests/k8s/save-config/namespace.yaml +4 -0
  152. data/tests/k8s/ssa/declared-bumped.yaml +22 -0
  153. data/tests/k8s/ssa/declared.yaml +22 -0
  154. data/tests/k8s/ssa/namespace.yaml +4 -0
  155. data/tests/k8s/ssa/seed-via-create.yaml +24 -0
  156. metadata +288 -20
  157. data/.travis.yml +0 -4
data/CHANGELOG.md ADDED
@@ -0,0 +1,642 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] - 2026-11-02
9
+
10
+ Architecture release: resources become pure manifest generators with
11
+ delivery moved into tasks, `App` becomes `Stack`, and the release adds
12
+ presenter pipelines over a pure transform layer, acceptors and
13
+ stack-config, and server-side apply with a seeding protocol for shared
14
+ field ownership. Several client-side-rolling-update-era surfaces are
15
+ removed. Review the Breaking and Removed sections before upgrading.
16
+
17
+ ### Breaking
18
+
19
+ - **The environment has exactly one source: the first positional
20
+ argument.** The `MATSURI_ENV` variable and symlink-name detection
21
+ (any binary name other than `matsuri` becoming the environment) are
22
+ removed. A per-environment entry point is argument injection in a
23
+ repo wrapper — `bin/staging` is `exec bin/matsuri staging "$@"`. The
24
+ command is now self-describing: what was typed, what `ps` shows, and
25
+ what matsuri resolved are the same string. `Initializer.start` takes
26
+ `(args)` instead of `(cmd, args)`. See docs/design/boot.md.
27
+ - **Delivery moves off the resource classes into `Matsuri::Tasks::KubectlOps`.**
28
+ Resources are pure manifest generators; the kubectl mechanism lives in
29
+ one task per verb (`Apply`, `Create`, `Replace`, `Delete`, `Recreate`,
30
+ `Scale`, plus the `Get` read), and the CLI selects the task. The
31
+ module-level boundary functions (`KubectlOps.apply/create/delete/
32
+ recreate/scale/converge`) are the single site deriving task inputs from
33
+ a rendered resource — post-generation manifest transforms will slot in
34
+ there. Consequences:
35
+ - `Kubernetes::Base` loses `create!`, `apply!`, `replace!`, `delete!`,
36
+ `recreate!`, `annotate!`, `converge!`, `converge_by_apply!`, and
37
+ `converge_by_recreate!`. Definitions overriding these must declare
38
+ delivery policy instead: `let(:converge_via)` (`:apply` default,
39
+ `:recreate`), `let(:apply_via)` (`:apply` default, `:replace`), and
40
+ `let(:save_config)` (default `true`). Pod, Secret, and ConfigMap
41
+ express their converge-by-recreate behavior through these lets.
42
+ - `annotate!` is removed outright: nothing bound it since `--record`
43
+ died, and deliberate annotation belongs to the manifest + apply path.
44
+ - `status!` is removed from `Kubernetes::Base`. It was a fatal stub no
45
+ resource ever implemented — every `status <type> <name>` died with
46
+ "I don't know how to display a status". The `Status` task renders
47
+ `kubectl get -o wide` instead, so the verb works for the first time.
48
+ `Stack#status!` remains the overridable hook for stack definitions.
49
+ - `Concerns::Scalable` is removed; `scale` is a task, and the CLI table
50
+ already names which workloads scale.
51
+ - `ImportedManifest` drops its five fatal verb stubs. Apply-only is now
52
+ structural — the CLI never bound the other verbs — and its custom
53
+ `apply!` is the same `Apply` task fed multi-doc YAML.
54
+ - Create/apply payloads are sent as YAML rather than JSON (one payload
55
+ shape serves single- and multi-doc manifests); semantics unchanged.
56
+ - `ExportManifest#applied_to_yaml` is fixed (previously raised
57
+ `NoMethodError`); it is now the canonical apply payload.
58
+ - `Kubernetes::Base#created?` is removed; the existence check is
59
+ `Tasks::KubectlOps.exists?(resource)` (the `Get` read task, which
60
+ `converge` already used). The verbose "already started / not
61
+ started" narration moves to `Get#exists?`. With it goes Base's
62
+ `ShellOut` include — resources no longer shell out at all.
63
+
64
+ - **`Concerns::ImportManifest` keeps only the import surface.** The
65
+ transform ops move to `Matsuri::Transform` (the concern includes the
66
+ Kubernetes dialect, so call sites inside definitions are unchanged),
67
+ and the set-carrier lookups (`lookup_manifests`, `without_manifest`,
68
+ criteria matching) stay. Two declarations change shape:
69
+ `with_resources` was a let holding a curried lambda
70
+ (`with_resources.(name, spec)`) and is now the dialect method
71
+ (`with_resources(name, spec)` — same returned step, no lambda-call on
72
+ the name); `node_selector_terms_path` was an overridable let and is
73
+ now the dialect constant `NODE_SELECTOR_TERMS_PATH`.
74
+
75
+ - **The client-side diff moves off the resource classes into
76
+ `Matsuri::Concerns::Diffable` + `Matsuri::Tasks::HashDiff`.** The
77
+ comparison is the resource's (`Diffable#diff(current)` takes the
78
+ manifest compared against and returns filtered deltas; the noise
79
+ filters remain the per-resource override surface); fetching the
80
+ current manifest and printing belong to the `HashDiff` task. This
81
+ diff is a permanent companion to the planned server-side
82
+ `KubectlOps::Diff` (`kubectl diff`), not a stopgap: with admission
83
+ controllers in play, the server-side diff shows what admission
84
+ injects, while this one shows drift relative to the declaration
85
+ with that noise filtered out. Consequences:
86
+ - `Kubernetes::Base` loses `diff!`, `diff` (zero-arg), `print_diff`,
87
+ and `current_manifest`. Library callers of `current_manifest` use
88
+ `Tasks::KubectlOps.manifest(resource)` (a plain parsed hash — the
89
+ `Map` wrapping and `raw:` flag are gone).
90
+ - The per-class `primary_container_diff`/`containers_diff` overrides
91
+ on Pod, ReplicaSet, and Deployment are replaced by a declarative
92
+ `let(:primary_container_path)`; the focused diff projects both
93
+ manifests by that path generically.
94
+ - `--primary-container` works for the first time on `pod` (previously
95
+ `NoMethodError`: `Pod#diff!` called a method that never existed)
96
+ and on `stateful_set`, `daemon_set`, and `cron_job` (previously
97
+ silently fell back to a full diff). All workloads with a pod
98
+ template now declare the path.
99
+ - `unit_comparison_is_equal?` no longer crashes on non-string values
100
+ (`Object#=~` was removed in Ruby 3.2; any diff touching a Hash or
101
+ Array value raised `NoMethodError` on modern Ruby).
102
+
103
+ - **Bare `diff` is now the server-side diff.** `matsuri <env> diff <type>
104
+ <name>` runs `kubectl diff` (`Tasks::KubectlOps::Diff`): the apiserver
105
+ dry-runs the same payload `apply` would send through defaulting and
106
+ admission, and the output shows live-vs-admitted — admission injections
107
+ included. `--hash-diff` restores the client-side filtered diff
108
+ (`Tasks::HashDiff`), which answers a different question: drift relative
109
+ to the declaration with cluster-owned noise filtered out. Both are
110
+ permanent. `--primary-container` is a HashDiff-only projection and
111
+ implies `--hash-diff`. kubectl's exit-code convention is honored: no
112
+ output and exit 0 means no differences; only exit codes above 1 are
113
+ errors.
114
+
115
+ - **`Matsuri::App` is renamed `Matsuri::Stack`.** A stack is a bundle of
116
+ resources managed together — one owner, one converge act; "app" never
117
+ described that. Registry type is `stack`, definitions load from
118
+ `platform/stacks`, the definition module is `Stacks`, and the CLI verbs
119
+ address stacks. There is no back-compat alias for the `app` type;
120
+ definitions must rename their directory and `Matsuri.define :app` calls.
121
+ - **Resources carry no imperative side-effects — the last cluster reads
122
+ move to `Tasks::Pod`.** `Pod#up?`, `Service#selected_pods`(`_json`),
123
+ and `Concerns::PodTemplate#selected_pods`(`_json`) — all dead since
124
+ resources stopped shelling out, with zero callers in the codebase —
125
+ are removed from the resource classes. `Tasks::Pod` carries them
126
+ instead, parameterized: `up?` takes `name:`/`namespace:`,
127
+ `selected_pods` takes `selector:` (a flat labels hash), and
128
+ `Tasks::Pod.pod_selector_for(resource)` derives that selector from
129
+ what each kind declares (workloads' `match_labels`, Service's
130
+ `selector`). Unwired by design — these are part of the incomplete
131
+ local-dev lifecycle work. `ShellOut` is now included exactly once in
132
+ the library, by `Matsuri::Task`: tasks are the only things that shell.
133
+
134
+ - **Read-back support is dropped in favor of seeds + server-side
135
+ apply.** Matsuri no longer reads live cluster state to make a
136
+ declaration decision. `Deployment#current_image`/`#current_image_tag`
137
+ are removed, along with the apply/create/recreate ladder rung that
138
+ backfilled a tag-less invocation from the live image — a tag-less
139
+ `apply deployment` now renders the acceptor default, same as `diff`
140
+ always did (the rung made diff and apply disagree for exactly the
141
+ invocations it served). The rolling-update-era `Deployment#update!`
142
+ and `#rollout!` (already broken since resources stopped shelling out)
143
+ are removed with it. The field the read-back protected is owned
144
+ structurally instead: a workload whose tag is rolled externally seeds
145
+ its image on create and omits it from the apply payload, so ongoing
146
+ applies have nothing to say about it — under client-side apply this
147
+ is pinned by `tests/bin/probe-save-config.sh`, and under an ongoing
148
+ deployment operator the tag belongs to that operator's field manager
149
+ via server-side apply.
150
+
151
+ - **`Matsuri::Stack` no longer shells out.** The `ShellOut` include is
152
+ gone, matching the resources. `docker!` remains on stacks as the hook
153
+ surface for definition-supplied `build!`/`push!` — it delegates to
154
+ `Tasks::Docker`, so definitions calling it are unchanged. A stack
155
+ definition that shelled with anything else (`kubectl!`, `sh_exec`)
156
+ must route through tasks (`Tasks::KubectlOps` for delivery verbs).
157
+
158
+ - **Workload `image_tag` defaults are removed.** Pod, Deployment,
159
+ ReplicaSet, ReplicationController, StatefulSet, DaemonSet, and CronJob no
160
+ longer default `image_tag` to `latest`; the default lives in the built-in
161
+ `Matsuri::Acceptors::ImageTag` registered under the stack's `:image_tag`
162
+ collection. Definitions instantiated outside a stack (bare `.new`,
163
+ library use) that relied on the implicit `latest` must declare their own:
164
+ `let(:image_tag) { options[:image_tag] || 'latest' }`. Resource-level
165
+ CLI verbs supply the tag explicitly and are unchanged.
166
+
167
+ - **Ingress emits `networking.k8s.io/v1`** (was `extensions/v1beta1`,
168
+ removed in Kubernetes 1.22). `spec.backend` is now
169
+ `spec.defaultBackend`; the `path` helper emits `pathType` (default
170
+ `Prefix`) and the v1 backend shape
171
+ (`service: { name:, port: { number: | name: } }` — an Integer port is
172
+ a number, anything else names a port); `ingress_class` now renders as
173
+ `spec.ingressClassName` instead of the retired
174
+ `kubernetes.io/ingress.class` annotation. Definitions that hand-rolled
175
+ `rules` or `default_backend` in the v1beta1 shape must move to the v1
176
+ shape.
177
+
178
+ ### Removed
179
+
180
+ - **`maybe_param_replicas`** on Deployment, ReplicaSet, and StatefulSet —
181
+ the client-side rolling-update era's `--replicas` passthrough, consumed
182
+ by nothing since `scale` became a task.
183
+ - **`Kubernetes::Base#build!`** — a NotImplementedError stub with no
184
+ caller: no CLI verb binds a per-resource build, and the working build
185
+ path has always been the stack-level hook (`Stack#build!`, supplied by
186
+ the stack definition and driven by `matsuri <env> build <stack>`).
187
+ A definition that implements its own `build!` is unaffected; direct
188
+ callers of the stub now get NoMethodError instead of
189
+ NotImplementedError.
190
+ - **`--record` everywhere kubectl is invoked** — create, apply, replace,
191
+ scale, imported-manifest apply, and `Deployment#rollout!`. kubectl
192
+ deprecated the flag (~1.22): it stamped the
193
+ `kubernetes.io/change-cause` annotation as a side effect of imperative
194
+ verbs, recording a command rather than an intent. Nothing replaced it
195
+ server-side — SSA's managedFields attributes who owns a field, not why
196
+ it changed. The intention here is to support
197
+ `kubernetes.io/change-cause` deliberately, as a post-generation
198
+ manifest transform stamping a platform-supplied value (release id,
199
+ ticket, ref) on the declarative path. The diff logic keeps treating
200
+ change-cause as an always-changed path.
201
+ - **ReplicationController support.** The resource type, its registry
202
+ entries and `rc` aliases, the `rc` subcommands on
203
+ show/diff/create/delete/recreate/apply/scale/status, the `rcs_path`
204
+ config key, and the `rollout` verb. `rollout` was the client-side
205
+ rolling update — mint `name-rN+1`, shell to `kubectl rolling-update` —
206
+ and `kubectl rolling-update` itself was removed from kubectl years
207
+ ago; the Deployment controller has owned rollouts since. Definitions
208
+ using `:rc` should move to Deployments (or ReplicaSets where the
209
+ controller semantics are genuinely wanted).
210
+ - **Support for legacy locally-bootstrapped Kubernetes.** Matsuri once
211
+ helped stand up a local cluster by hand — etcd, hyperkube, and SkyDNS
212
+ on the dev machine — from before minikube and k3s made that someone
213
+ else's job. The DNS add-on (`Matsuri::AddOns::DNS`, a SkyDNS/kube2sky
214
+ deployment pinned to Kubernetes v1.0) and its `show dns` /
215
+ `create dns` / `delete dns` verbs are removed, along with the
216
+ bootstrap-era config keys: `etcd_version`, `hyperkube_version`,
217
+ `kube2dns_version`, `skydns_version`, `kubernetes_cidr`, `etcd_addr`,
218
+ `etcd_bind_addr`, `master_addr`, `master_bind_addr`, `api_servers`,
219
+ `master_url`, `cluster_dns`, and `cluster_domain`. `dev_addr` stays —
220
+ Endpoints definitions still default `public_ip_address` to it.
221
+ - **`let(:release)` and `maybe_param_name`.** Both served the client-side
222
+ rolling-update machinery and lost their last consumer with it.
223
+ `release` was the incrementing `-rN` rollout counter, defined on every
224
+ resource (default `'0'`) and forwarded through pod options; nothing
225
+ read it. `maybe_param_name` let `options[:name]` override the manifest
226
+ name so a successor controller could be minted alongside the current
227
+ one — no caller ever passed it to Deployment, ReplicaSet, or
228
+ StatefulSet, and dropping it collapses their `default_metadata`
229
+ overrides back to the base (which also restores `final_annotations`
230
+ assembly on those three kinds). Its real requirement — apply should
231
+ not clobber live replica counts set by an HPA or manual scale — is
232
+ answered structurally by the seed family: the manifest omits the
233
+ field, so apply has nothing to say about it.
234
+
235
+ ### Changed
236
+
237
+ - **Unknown `apply_via` values are fatal.** Delivery routing is a total
238
+ case over `:apply` / `:replace` / `:server_side_apply`; a typo'd value
239
+ raises instead of silently falling through to client-side apply and
240
+ delivering under the wrong merge semantics.
241
+ - **create/recreate consult last-applied policy before seeding it.**
242
+ `--save-config` is gated by `consults_last_applied?(apply_via)`:
243
+ resources delivered by `:replace` or `:server_side_apply` create with
244
+ `--save-config=false` regardless of their `save_config`, because a
245
+ seeded last-applied is a prune trap for any later client-side apply.
246
+ - **The `latest` positional default has one owner.** The resource-level
247
+ CLI verbs (`show`/`diff`/`apply`/`create`/`recreate` with an image-tag
248
+ positional) no longer hardcode `'latest'` per cmd file; the positional
249
+ defaults to nil and routes through the built-in `ImageTag` acceptor —
250
+ the same single owner the stack `:image_tag` collection uses. With
251
+ read-back dropped, every verb resolves the tag the same way:
252
+ positional, else the acceptor default.
253
+ - **ConfigMap applies normally.** The replace-instead-of-apply override
254
+ dated to 2016 kubectl issues (kubernetes#32432, kubernetes#41711) that
255
+ kubectl fixed years ago; `kubectl apply` handles configmaps fine. Apply
256
+ now goes through `apply` like every other resource, and create seeds
257
+ `last-applied-configuration` (`--save-config`) so the three-way merge
258
+ has its base. Converge-by-recreate policy is unchanged.
259
+ - **Untagged stack converges propagate `latest` explicitly** (previously
260
+ propagated nil and each workload class defaulted individually). Rendered
261
+ output is unchanged; drift between per-class defaults is no longer
262
+ possible.
263
+ - **Declared holes raise `Matsuri::UndefinedLet` instead of
264
+ `NotImplementedError`.** Ruby reserves `NotImplementedError` for
265
+ features the current platform does not implement, and it descends from
266
+ `ScriptError`, so a bare `rescue` lets it fly past.
267
+ `UndefinedLet < NoMethodError` names what actually happened: the
268
+ definition has no working version of that let. The raise lands in the
269
+ code that expected the value, so the implementer can inspect that
270
+ context and see what the definition needs. Specs
271
+ asserting on unfilled holes should expect the new class. Relatedly,
272
+ `Stack#status!` (an abstract method, not a let) raises `NoMethodError`
273
+ directly, and the `ImportedManifest` namespace hole — previously a call
274
+ to the undefined `Matsuri.fail`, which died as the wrong `NoMethodError`
275
+ with its message lost — raises `UndefinedLet` with its message intact.
276
+
277
+ ### Added
278
+
279
+ - **`Presenters::PruneEmpty`** — prunes empty values (`nil`, `{}`, `[]`)
280
+ from payloads, bottom-up; empty strings survive and array elements are
281
+ never dropped. Under server-side apply an empty collection is an
282
+ ownership claim over a field the definition does not manage, and
283
+ inside an atomic struct (`.spec.selector`) it diverges from the
284
+ server's persisted normal form, breaking the seeding protocol's
285
+ equal-values co-ownership with a conflict. Declared on both intent
286
+ regimes, a pruned payload declares in persistence normal form: ceding
287
+ a field and saying nothing are the same act. `let(:exceptions)` names
288
+ paths kept even when empty (ships with none); the `prune_empty(except:)`
289
+ helper closes it declaration-side. See docs/presenters.md.
290
+ - **The repo's `lib/` joins the load path at boot.** Immediately after
291
+ repo discovery, the initializer prepends `Config.lib_path` (the key
292
+ existed, nothing consumed it), so a repo's own extension library
293
+ resolves identically under `bundle exec`, a bare gem install, or a
294
+ checkout — no gemspec and no wrapper-exported load paths. Boot
295
+ promise layers are documented in docs/design/boot.md.
296
+ - **Server-side apply.** `apply_via :server_side_apply` delivers through
297
+ `Tasks::KubectlOps::ServerSideApply` (`kubectl apply --server-side
298
+ --field-manager=<m>`); the manager name comes from the resource's
299
+ `field_manager` let, defaulting to the repo-wide
300
+ `Matsuri::Platform.field_manager` (default `matsuri`). The name
301
+ declares the repo's ongoing-delivery identity: the conflict frontier
302
+ forms exactly where manager names differ, and a shared name is a
303
+ single manager whose writes never conflict within it. Conflicts stay
304
+ loud — `--force-conflicts` is never passed; forcing through a frontier
305
+ is an incident-time human judgment made with raw kubectl. `diff` on a
306
+ server-side resource routes `--server-side` under the same manager, so
307
+ the preview shows the same conflict frontier delivery will hit
308
+ (conflicts surface as diff errors). Usage is documented in
309
+ docs/ssa.md, the constraints in docs/design/ssa.md.
310
+ - **`Tasks::Stacks::Seed` — the creation protocol for
311
+ server-side-applied resources.** Server-side apply cannot safely
312
+ create an object whose payload carries seeds: seeds sent by apply are
313
+ owned and then pruned by the first omitting apply, and an apply
314
+ against empty managedFields attributes every pre-existing field to the
315
+ synthetic manager `before-first-apply`, which never relinquishes. The
316
+ protocol: create the seeded payload as `<field_manager>-seed` (the
317
+ seed owner, no `--save-config`), deliver the declared payload with an
318
+ ordinary server-side apply, then atomically remove the seed owner's
319
+ managedFields entry (a test+remove JSON patch — a stale index is a
320
+ loud rejection with a bounded reread retry, never a wrong removal).
321
+ The field manager ends sole owner of the declared fields; seeds end
322
+ unowned until a controller or deliverer takes them. The delivery verbs
323
+ run the protocol themselves: `apply` (and so converge) routes a
324
+ not-yet-live server-side resource through `Seed`, `create` brings one
325
+ into existence through it, and `recreate` tears down and reseeds —
326
+ every later apply is the ordinary mechanism, and sequential
327
+ `build_order` means a member that hands a field to another manager can
328
+ simply ride later in the stack.
329
+ `tests/bin/probe-ssa.sh` pins the protocol and the refuted
330
+ alternatives against a live apiserver.
331
+ - **The seed family.** `Matsuri::Presenters::SeedReplicas` / `SeedImage`
332
+ / `SeedResources` — concern + declared-hole pairs that complete a
333
+ create payload with a field's boot value, with `seed_replicas(n)` /
334
+ `seed_image(img)` / `seed_resources(spec)` declaration-surface
335
+ constructors on stacks and resources. Additive on create: a field only
336
+ seeds when the manifest omits it, so a declared field is the opt-out
337
+ and nothing ever overwrites a declaration; the `:on_apply` payload
338
+ never carries seeded fields, so controllers (HPA) take ownership
339
+ uncontested — the cluster mechanics are pinned by
340
+ `tests/bin/probe-save-config.sh`. `save_config` becomes a resolution
341
+ chain, most-specific-first: a definition's own let, the owning stack's
342
+ `save_config false` declaration, then repo-wide
343
+ `Matsuri::Platform.save_config` (default `true`); normal apply path
344
+ only, never crossing nested stacks. The dialect gains
345
+ `pod_spec_path(manifest)` — the pod spec within a pod-template
346
+ workload or bare pod. See the Seeds section of docs/presenters.md.
347
+ - **`tests/` — a live-cluster probe harness.** `bin/minikube-start.sh`
348
+ runs a dedicated `matsuri` minikube profile (`--keep-context`; vfkit
349
+ driver on macOS), and `bin/probe-save-config.sh` pins the delivery
350
+ semantics the seed model rests on: a create without `--save-config`
351
+ keeps seeded fields out of last-applied, so ongoing applies of a
352
+ manifest omitting them never prune them, and a controller takes
353
+ ownership uncontested. minikube, kubectl, and (on macOS) vfkit join
354
+ the nix devshell. These are delivery-semantics probes against a real
355
+ API server, deliberately outside the rspec suite.
356
+ - **`present` pipelines on resources.** The same `present` grammar on
357
+ `Kubernetes::Base`; default none. Through a stack, the two declaration
358
+ sites merge into one pipeline — resource members first, stack members
359
+ last. Both run; position is who sees the completed manifest: the
360
+ resource's pipeline completes its self-knowledge, the stack's steps
361
+ admit the finished manifest (where the server's own mutating admission
362
+ sits relative to the client's payload). Direct resource verbs assemble
363
+ the resource's own declarations alone, so a resource behaves
364
+ identically converged directly or through an undeclared stack.
365
+ - **`present` pipelines on stacks.** `present(scenario, *members)`
366
+ appends transform members to a scenario's pipeline — unlike `accept`,
367
+ a scenario accumulates, and declaration order is member order. Members
368
+ are presenter names, classes, Callables, or anything `#call`-able; the
369
+ combinator vocabulary (`match`, `match_kind`, `match_deployments`) is
370
+ available in the stack class body. Scenarios are intent regimes, not
371
+ verbs: `:on_create` (brought into existence — `create`, recreate's
372
+ create leg) and `:on_apply` (delivered to something existing —
373
+ `apply`, `replace`, `diff`). `apply_via` picks how the payload
374
+ travels; the scenario picks what it says — flipping delivery mechanism
375
+ never changes which presenters run. Declarations ride to the
376
+ `KubectlOps` boundary as pure data and each verb assembles its own
377
+ scenario there; one site builds the presented payload for both diff
378
+ and apply, so diff previews delivery through presenters. The empty
379
+ pipeline is the identity: undeclared stacks send byte-identical
380
+ payloads. Declarations never cross a stack boundary. Multi-doc
381
+ manifests present per document. See docs/presenters.md. Boundary
382
+ consequences: the module functions serialize payloads from
383
+ `*_manifest_for_export` themselves (resources' `to_yaml`/
384
+ `applied_to_yaml` overrides no longer shape delivery payloads, only
385
+ `show`), and `converge` grew a `presenters:` keyword — a literal
386
+ trailing-hash `converge(r, restart: true)` call now needs braces.
387
+ The client-side HashDiff diffs against the presented declaration too:
388
+ it assembles the same `:on_apply` pipeline over the same basis apply
389
+ sends — the applied manifest (stack declarations ride in from
390
+ `diff stack`; direct verbs merge the resource's own) — so
391
+ `--hash-diff` drift reads against what apply would send: a value a
392
+ presenter pins is not drift, a field the resource restricts out of
393
+ `applied_manifest` is not drift, and drift the pipeline does not
394
+ account for still reports. `Diffable#diff`/`#primary_container_diff`
395
+ accept the basis as `desired:`.
396
+ - **`Matsuri::Presenter` — zero-config, context-free manifest steps.** A
397
+ presenter is Manifest→Manifest: construction takes nothing, and
398
+ parameterization happens before `#call` ever runs — definition lets
399
+ filling a concern's declared holes, or declaration-time values closed
400
+ over in a leaf `Callable`. Authors implement `#transform`; `#call` is
401
+ framework-owned and makes the one deep boundary copy (Marshal, because
402
+ the carrier is data), so input-never-mutated is a constraint rather
403
+ than a convention, and a fresh instance per call scopes let memoization
404
+ to a single manifest pass. The Kubernetes dialect is bare vocabulary
405
+ inside `#transform`. Registry type `presenter`, definitions in
406
+ `platform/presenters`, `Registry.presenter(name)` reader, bare classes
407
+ equally valid. No built-ins ship yet.
408
+ - **`Transform::Kubernetes` — the manifest dialect.** Algebra + Env
409
+ addressed to Kubernetes shapes: `set_env`, `set_all_env`,
410
+ `set_resources` (strict, `EntryNotFound` on unknown container) and its
411
+ curried `with_resources`, `set_label`, `append_volume`,
412
+ `append_volume_mount`, `without_container`, and the node-affinity
413
+ helpers over a well-known path constant — a misspelled path cannot
414
+ exist at a call site. Pod now draws `expand_env` from `Transform::Env`
415
+ instead of defining its own (`sorted_env` unchanged).
416
+ - **`Transform::Combinators` — Element→Element machinery.** `Match`
417
+ applies its element only to manifests meeting the declared
418
+ `{kind:, name:, namespace:}` criteria (the import-lookup addressing
419
+ grammar; unknown keys are an error) and is the identity otherwise.
420
+ The `Helpers` module carries the declaration vocabulary — `match`,
421
+ `match_kind`, `match_deployments`; sugars grow on demand. Combinators
422
+ are never registry-named: names cross the declaration boundary,
423
+ machinery does not.
424
+ - **`Matsuri::Transform` — the pure manifest-transform strata.** Nothing
425
+ under the namespace shells out; everything operates on plain data and
426
+ composes as values. `Transform::Algebra` carries the path-addressed ops
427
+ (`update_in`, `append_to`, `update_each`, `append_each`, `index_by_name`
428
+ plus a strict `index_by_name!`, `except_entry_by_name`); contributions
429
+ are values composed by `+`, never super()-chained deltas.
430
+ `Transform::Callable` is the free monoid over transforms: `new` =
431
+ identity, `new(label:) { }` = leaf, `new([members])` = composite, `+` =
432
+ operand member-list concat into a new instance — never flattened, so a
433
+ composite member stays one member and its label rides along as trace
434
+ metadata. Coercion happens at construction and fails fast (#call-ables
435
+ as-is; Symbol/String will name registered presenters). `Transform::Env`
436
+ is the array↔map isomorphism for container env entries. Errors
437
+ (`EntryNotFound`, `PathNotFound`) raise rather than log fatal. Inert in
438
+ this phase: nothing calls the strata yet; `Concerns::ImportManifest` and
439
+ Pod's env helpers are untouched until they rebuild on top. Law specs pin
440
+ identity, associativity, zero-point, structural homomorphism,
441
+ no-flatten representation, carrier-is-data, and the env round trip.
442
+ - **Acceptor collections on stacks.** `accept(collection, acceptor)`
443
+ registers an acceptor (class or registry name) under a named collection;
444
+ last declaration per collection wins, so re-declaring overrides. The
445
+ collection name is the options key: each acceptor admits its own
446
+ component of the invocation options, whole, and cannot see another
447
+ collection's. `Matsuri::Stack` ships two built-ins — `:image_tag`
448
+ carries the converge positional (a scalar, admitted whole, owning the
449
+ `latest` default) and `:config` carries the stack-config artifact (the
450
+ built-in is the empty acceptor: the collection is total, its surface is
451
+ empty until a platform re-declares it).
452
+ - **stack-config.** `--stack-config=PATH` on `converge` and the new
453
+ `show stack` / `diff stack` verbs names a YAML artifact for the stack
454
+ instance being addressed. The CLI owns the boundary mapping — it loads
455
+ the file and supplies data under the `:config` key; the stack itself
456
+ never loads anything, so specs, controllers, and tooling construct
457
+ stacks with data directly. A platform declares its accepted surface
458
+ with `accept :config, <acceptor>`; resource members read the bound
459
+ acceptor instance as `stack_config` (nil when a definition is invoked
460
+ standalone), and the pod template forwards it, so pod definitions read
461
+ the same binding. `Stack#delete!` instantiates members with the same
462
+ options as converge, so a member whose identity derives from an
463
+ accepted value addresses the same resource it created. No flag is the
464
+ empty value and every declared read lands on its default; a flag with a
465
+ bad path fails loudly, never falls back. stack-config never crosses a
466
+ stack boundary — nested stack deps do not inherit it.
467
+ - **Acceptor** — a class that admits a value whose vocabulary matsuri does
468
+ not own, such as a per-instance config artifact. The raw value arrives
469
+ whole as `options`; `let` declarations are the accepted surface.
470
+ Undeclared reads raise `NoMethodError`, unconsumed fields pass in
471
+ silence, and every declared read carries its default, so the empty value
472
+ is valid. Registry type `acceptor`, definitions in `platform/acceptors`,
473
+ bare classes equally valid. See docs/acceptors.md and
474
+ docs/design/acceptors.md.
475
+
476
+ ### Fixed
477
+
478
+ - **The rollout watch returns as a task.**
479
+ `Tasks::KubectlOps.rollout_status(resource)`
480
+ (`kubectl rollout status`) is the post-apply progress watch for
481
+ manually delivered changes. `Deployment#watch_rollout!` — dead since
482
+ resources stopped shelling out — is removed rather than revived:
483
+ resources are pure generators and carry no verbs, delegating ones
484
+ included. Callers address the boundary function with the resource.
485
+ - **Two latent `ImportManifest` bugs die by construction with the move
486
+ to the strata.** `append_node_affinity` referenced a misspelled path
487
+ name and raised `NameError` on every call; the dialect reads the
488
+ `NODE_SELECTOR_TERMS_PATH` constant, so a misspelled path cannot
489
+ exist at a call site. `set_env` from an imported manifest crashed
490
+ because `update_kv` called `expand_env`, which only existed on Pod;
491
+ both now live in `Transform::Env`, and the vendor-app fixture pins
492
+ the repaired path end-to-end.
493
+
494
+ ## [0.2.0] - 2026-10-24
495
+
496
+ Modernization release: the gem now targets Ruby >= 3.2 (developed and tested
497
+ on Ruby 4.0), drops ActiveSupport, and fixes a series of latent
498
+ manifest-rendering bugs — several of which change rendered output or diff
499
+ behavior. Review the Breaking and Changed sections before upgrading.
500
+
501
+ ### Breaking
502
+
503
+ - **Ruby floor is now 3.2** (`required_ruby_version >= 3.2`). Developed and
504
+ tested on Ruby 4.0. Ruby 2.x is no longer supported.
505
+ - **ActiveSupport is no longer a dependency.** The nine core_ext slices
506
+ matsuri used (deep key transforms, `try`, `blank?`/`present?`/`presence`,
507
+ `camelize`, `class_attribute`) are vendored in `matsuri/core_ext` as
508
+ monkeypatches, so definition files keep their ambient grammar. Downstream
509
+ repos that relied on matsuri transitively loading the rest of ActiveSupport
510
+ must now require it themselves.
511
+ - **Pod security context lets renamed**: `priviledged` →
512
+ `privileged`, `allow_priviledge_escalation` → `allow_privilege_escalation`.
513
+ The old misspelled keys rendered field names the SecurityContext API does
514
+ not have (latent while nil-and-compacted); definitions overriding the old
515
+ lets must rename them.
516
+ - **`frozen_string_literal: true` across the codebase.** Strings returned by
517
+ matsuri lets are frozen; definition code that mutated them in place will
518
+ raise `FrozenError`.
519
+ - `awesome_print` removed (debugging residue in `imported_manifest`, never a
520
+ declared dependency).
521
+
522
+ ### Changed
523
+
524
+ Semantics changes — same DSL, different rendered output or runtime behavior:
525
+
526
+ - **`mem_request` now defaults to `mem_limit`** (previously defaulted to
527
+ `cpu_limit`, a copy-paste error): pods defining only limits requested
528
+ cpu-shaped quantities as memory.
529
+ - **Diff output is stricter.** The nil-vs-zero diff filter is now scoped to
530
+ `spec.minReadySeconds` as originally intended (a missing `and` had made the
531
+ path comparison a void expression). Declared zeros against a cluster nil —
532
+ e.g. `runAsUser: 0` — now surface on all other paths.
533
+ - **Binding subjects render `apiGroup`** (camelCase) instead of a snake_case
534
+ `api_group` field that lenient decoding silently dropped. ServiceAccount
535
+ subjects had survived on the implicit `""` default; User and Group subjects
536
+ would fail reconcile on strict decoding. The DSL-facing `api_group:`
537
+ parameter is unchanged.
538
+ - **Roles without `bind_to` no longer emit bindings with empty subjects**;
539
+ the subjects guard in `Binding#rbac_manifests` is honored, and subject-less
540
+ bindings return nil (not false) so they compact away instead of crashing
541
+ downstream key stringification.
542
+ - **ServiceAccount collections (`secrets`, `imagePullSecrets`) render only
543
+ when non-empty.** An explicit empty list in applied configuration claims
544
+ field ownership and deletes entries other actors add; an absent field makes
545
+ no claim.
546
+ - **CronJob `timeZone`** renders the correct camelCase API field (the old
547
+ snake_case key was invisible while unset, wrong the moment anyone set it);
548
+ the job template spec now compacts, no longer emitting explicit nulls that
549
+ strategic merge reads as field clears. The `sucess_policy` let is renamed
550
+ `success_policy`.
551
+ - **Metadata default/override/final triple completed** in the cluster DSL:
552
+ `final_metadata` is the user merge hook, and Binding, Role, ClusterRole,
553
+ and ServiceAccount consume it. Rendered output is unchanged for existing
554
+ definitions. `default_metadata` now compacts (cluster-scoped resources stop
555
+ carrying a null `namespace` key into last-applied state), and namespaces
556
+ coerce with `&.to_s` so symbol namespaces render identically on the YAML
557
+ and JSON paths.
558
+ - Pod affinity `label_selector` compacts: affinity terms no longer render an
559
+ explicit `matchExpressions` null.
560
+
561
+ ### Added
562
+
563
+ - **`Concerns::Pods::SpreadPods`** — opt-in pod anti-affinity vocabulary for
564
+ spreading same-labeled pods across nodes or zones. Not included in Pod by
565
+ default; the includer selects a strategy via `let(:pod_anti_affinity)`.
566
+ The zone topology key is its own let, defaulting to the well-known
567
+ `topology.kubernetes.io/zone`.
568
+ - **`Concerns::Pods::SpreadTopology`** — opt-in topology spread constraint
569
+ vocabulary complementing SpreadPods: constraints express balance (bounded
570
+ replica skew per domain) where anti-affinity expresses exclusion. One
571
+ `balanced_spread` builder specialized by topology-key lets; knobs ladder
572
+ from shared defaults (`spread_max_skew`, `spread_when_unsatisfiable`) to
573
+ per-domain overrides; strategies return lists and compose by
574
+ concatenation. `zone_topology_key` is shared with SpreadPods so one
575
+ override re-points both mechanisms.
576
+ - **CronJob** resource.
577
+ - **ImportedManifest** resource with `show`/`apply` command support, plus
578
+ `ImportManifest` manifest-transform helpers and `ExportManifest`
579
+ (refactored from `TransformManifest`).
580
+ - Pod template helpers; updated volume mount helper.
581
+ - Golden-manifest spec suite replacing the untouched gem-template skeleton:
582
+ fixture-driven goldens for pod, deployment, stateful set, cron job, and the
583
+ cluster RBAC DSL; specs for the vendored core_ext shims; load-order specs
584
+ that touch each entry constant first in a fresh process (two load-order
585
+ bugs surfaced during conversion); worked examples of bulkhead deployments
586
+ over a shared podspec with a two-axis identity scheme (`app_role` ×
587
+ `resource_class`) and a selector pairwise-disjointness law.
588
+ - `base64` gemspec dependency (left Ruby's default gems in 3.4; `secret` and
589
+ `config_map` fail to load under bundler without it).
590
+ - Nix flake development environment.
591
+
592
+ ### Fixed
593
+
594
+ - `Matsuri.define(:type, name, inherits: parent)` raised `ArgumentError` on
595
+ Ruby 3+: keyword separation turned kwargs passed through the bare `*args`
596
+ forwarder into a positional hash. The facade now mirrors
597
+ `Registry.define`'s real signature.
598
+ - **`AggregatedClusterRole` is now actually defined.** Its file reopened
599
+ `ClusterRole` instead of defining its own class, so the autoload constant
600
+ never resolved — and loading the file would have replaced the regular
601
+ ClusterRole manifest (aggregationRule in place of rules) for every cluster
602
+ role.
603
+ - **The namespace/scope cluster DSL was broken end to end**: `scope()`
604
+ referenced a wrong constant, discarded the child scope instead of appending
605
+ it to definitions, and `require_namespace!` ignored the enclosing namespace
606
+ scope its own error message offered. `aggregated_cluster_role()` now
607
+ instantiates `AggregatedClusterRole`; it previously built a plain
608
+ ClusterRole whose block would fail on the `match` helper.
609
+ - ServiceAccount `image_pull_secret` and `secret` helpers pushed into
610
+ singular keys that initialize never created, raising `NoMethodError`; the
611
+ storage and k8s fields are plural.
612
+ - `maybe_param_replicas` read `options[:relicas]` in all four workload
613
+ classes, so passing `replicas` through options never worked.
614
+ - HPA: `average_value_metric_target` typo.
615
+ - Comparing a unit against an empty value no longer errors.
616
+ - Config: capital-F `Fail` in the `base_path` default raised `NoMethodError`
617
+ instead of the intended message.
618
+ - Registry `options_for_type` listing: the basename/uniq/sort chain applied
619
+ only to the second glob, so "Which <type>?" listings printed full paths for
620
+ environment-dir definitions and never deduplicated.
621
+ - `Matsuri::Cmds::Cli` subclasses Thor but never required it, freeloading off
622
+ the initializer; loading it first raised `NameError`.
623
+ - App: removed a duplicate `build!` stub silently shadowed by the later
624
+ definition, and dead `call_on_failure` referencing a `failure_hooks` that
625
+ was defined nowhere.
626
+ - `rlet/lazy_options` alone cannot resolve `Concern` under rlet 0.7;
627
+ `app.rb` now requires the rlet umbrella so Registry/App load as a library
628
+ without the CLI.
629
+
630
+ ### Removed
631
+
632
+ - ActiveSupport dependency (see Breaking).
633
+ - `awesome_print` require (see Breaking).
634
+ - `.travis.yml` (targeted ruby 2.2.2 and bundler 1.10.6).
635
+
636
+ ## [0.1.6] - 2022-03-24
637
+
638
+ Last release before this changelog was introduced. See git history.
639
+
640
+ [0.3.0]: https://github.com/matsuri-rb/matsuri/compare/v0.2.0...v0.3.0
641
+ [0.2.0]: https://github.com/matsuri-rb/matsuri/compare/v0.1.6...v0.2.0
642
+ [0.1.6]: https://github.com/matsuri-rb/matsuri/releases/tag/v0.1.6
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in matsuri.gemspec