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/README.md CHANGED
@@ -1,8 +1,56 @@
1
1
  # Matsuri
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/matsuri`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
3
+ Matsuri manages Kubernetes manifests. Matsuri uses the full Ruby
4
+ language for generating manifests. Unlike Helm, it constructs these with
5
+ Ruby objects instead of using string-based templating.
6
+
7
+ Resources are defined by `let()` extracted from `rspec`. These values
8
+ can be applied monoidally in a flat keyspace, through mixins
9
+ (`Concerns`) or Ruby inheritance. It ultimately constructs a hash data
10
+ structure which the harness feeds into `kubectl` to apply to a target
11
+ cluster.
12
+
13
+ Since Matsuri *generates* manifests from hash objects, values can be
14
+ safely inserted. Fragments are just more hash objects. Helper methods
15
+ can be written. [Acceptors](docs/acceptors.md) can be defined to import
16
+ configuration values and resource definitions can inject them in.
17
+
18
+ Similar to Kustomize's design, Matsuri also includes a full transform
19
+ algebra, both for importing upstream manifests, as well as applying
20
+ transforms after generation. [Presenters](docs/presenters.md) can be
21
+ used to create seed values on creation. Subsequent applications through
22
+ Server Side Apply can omit seed presenters, allowing Matsuri to cede
23
+ those values for other operators to manage.
24
+
25
+ The final hashes are encoded as JSON or YAML, and not before. This
26
+ allows Matsuri to inject, manipulate, and transform manifests as
27
+ structured data, instead of as strings.
28
+
29
+ Finally, Matsuri provides a [Stack](docs/stacks.md) abstraction. These
30
+ bundle together a collection of resources that can be applied together.
31
+ When combined with acceptors, this allows for automatically standing up
32
+ preview environments.
33
+
34
+ ## History
35
+
36
+ Matsuri was originally created to help stand up a local K8S cluster for
37
+ a dev environment, while also managing deployments to a staging and
38
+ production environment. Better tools, such as `k9s` and `minikube`, have
39
+ since been developed by the Kubernetes community.
40
+
41
+ While Helm has emerged as the de facto industry standard, and kustomize
42
+ closes many of Helm's design flaws, Matsuri can do something neither do
43
+ well: it can dynamically generate whole stacks.
44
+
45
+ Unlike cdk8s, Matsuri's use of monoidal merges allows for a far greater
46
+ degree of composability and extensibility. Its tradeoff is the use of
47
+ Ruby, which not all platform engineering teams may have expertise in.
48
+
49
+ In an era of AI coding assistance, platform teams can leverage coding
50
+ agents to assist with using Matsuri. The current release includes the
51
+ design grammar, rspec demonstrating golden path patterns, and usage
52
+ documentation to help get things started. Consider using Matsuri if you
53
+ need to dynamically set up Kubernetes resources.
6
54
 
7
55
  ## Installation
8
56
 
@@ -22,13 +70,43 @@ Or install it yourself as:
22
70
 
23
71
  ## Usage
24
72
 
25
- TODO: Write usage instructions here
73
+ The documentation covers the golden paths:
74
+
75
+ - [Setup](docs/setup.md) — repo layout, environments, kubectl contexts,
76
+ and namespaces
77
+ - [Boot](docs/boot.md) — invocation and the boot sequence
78
+ - [Resources](docs/resources.md) — defining resources with `let()`
79
+ - [Stacks](docs/stacks.md) — bundling resources and converging them
80
+ together
81
+ - [Acceptors](docs/acceptors.md) — importing configuration values
82
+ - [Presenters](docs/presenters.md) — transforms, seeds, and server-side
83
+ apply
84
+ - [Server-Side Apply](docs/ssa.md) — field ownership, normal form, and
85
+ the creation protocol
86
+ - [Diff](docs/diff.md) — previewing what converge will send
87
+ - [Local Development](docs/local-dev.md) — the local-dev workflows
88
+ - [Known Issues](docs/known-issues.md)
89
+
90
+ Design notes — the constraints and the properties they induce — live in
91
+ [docs/design](docs/design).
26
92
 
27
93
  ## Development
28
94
 
29
- 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.
95
+ There is a Nix flake containing the dependencies for developing Matsuri.
96
+ This includes Ruby and minikube. A `.envrc` is set up for `direnv`
97
+ users. The gems are not a part of the flake so you will still have to
98
+ run
99
+
100
+ bundle install
101
+
102
+ Coding agents can invoke commands with
30
103
 
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
104
+ direnv exec . bundle exec rake
105
+
106
+ To release a new version, update the version number in `version.rb`, and
107
+ then run `bundle exec rake release`, which will create a git tag for the
108
+ version, push git commits and tags, and push the `.gem` file to
109
+ [rubygems.org](https://rubygems.org).
32
110
 
33
111
  ## License
34
112
 
@@ -36,5 +114,5 @@ Matsuri is distributed under the MIT License
36
114
 
37
115
  ## Contributing
38
116
 
39
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/matsuri.
40
-
117
+ Bug reports and pull requests are welcome on GitHub at
118
+ <https://github.com/matsuri-rb/matsuri>.
data/Rakefile CHANGED
@@ -1,6 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
8
+ namespace :docs do
9
+ desc "Reformat docs/**/*.md in place with pandoc (gfm, 72 columns)"
10
+ task :fmt do
11
+ FileList["docs/**/*.md"].each do |path|
12
+ formatted = IO.popen(%w[pandoc -f gfm -t gfm-smart --columns=72], "r+") do |io|
13
+ io.write File.read(path)
14
+ io.close_write
15
+ io.read
16
+ end
17
+ fail "pandoc failed on #{path}" unless $?.success?
18
+ # pandoc writes "``` ruby"; conventional GFM is "```ruby"
19
+ formatted = formatted.gsub(/^(\s*)``` (\S)/, '\1```\2')
20
+ File.write(path, formatted)
21
+ puts path
22
+ end
23
+ end
24
+ end
25
+
6
26
  task :default => :spec
data/bin/console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require "bundler/setup"
4
5
  require "matsuri"
data/bin/matsuri ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'matsuri'
5
+
6
+ Matsuri::Initializer.start(ARGV)
data/bin/setup CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/bin/bash
2
+ # frozen_string_literal: true
3
+
2
4
  set -euo pipefail
3
5
  IFS=$'\n\t'
4
6
 
data/docs/acceptors.md ADDED
@@ -0,0 +1,181 @@
1
+ # Acceptors
2
+
3
+ An acceptor maps the information from a different bounded context into
4
+ the bounded context of Matsuri -- K8S resources and Matsuri abstractions
5
+ such as `stacks`.
6
+
7
+ The promises:
8
+
9
+ - Fields the acceptor does not declare are ignored. Upstream can add
10
+ vocabulary freely without breaking anything downstream.
11
+ - Reads the acceptor does not declare raise `NoMethodError`. Nothing
12
+ reaches past the declared surface.
13
+ - There is no fallback to the underlying value. When upstream renames a
14
+ field, the remap happens in the acceptor, in one place, and consumers
15
+ do not move.
16
+
17
+ ## Defining Acceptors
18
+
19
+ Acceptors are defined in `platform/acceptors`. The raw value arrives
20
+ whole as `options`; each `let` declares one read on the accepted
21
+ surface.
22
+
23
+ ```ruby
24
+ Matsuri.define :acceptor, 'app' do
25
+ # the artifact anchor owns the empty value
26
+ let(:artifact) { options || {} }
27
+
28
+ # identity
29
+ let(:instance_name) { artifact[:name] }
30
+ let(:namespace) { artifact[:namespace] }
31
+
32
+ # declared settings, with the defaults this surface promises
33
+ let(:log_level) { artifact[:log_level] || 'info' }
34
+ let(:worker_queue) { artifact[:worker_queue] || 'default' }
35
+
36
+ # a reference: the default points at the shared tier, an entry in the
37
+ # artifact overrides it
38
+ let(:search_api_url) do
39
+ (artifact[:pins] || {})['search-api'] || 'https://search.staging.svc.cluster.local'
40
+ end
41
+ end
42
+ ```
43
+
44
+ Keep the declared surface to flat scalar values. Lets that return nested
45
+ structures will work, but you give up treating an instance config as a
46
+ sparse overlay of the defaults — and deep-merging nested returns is
47
+ where that goes wrong.
48
+
49
+ Because every declared read carries its default, an empty value is a
50
+ valid one: every read lands on its default. A sparse artifact overrides
51
+ only what it names.
52
+
53
+ The empty value belongs to the acceptor, because what empty means is
54
+ specific to the vocabulary it admits: every declared read is total over
55
+ nil. A hash-shaped surface anchors once —
56
+ `let(:artifact) { options || {} }` — and reads fields off the anchor; a
57
+ scalar surface reads `options` whole, as the built-in image tag acceptor
58
+ does.
59
+
60
+ ## Reading
61
+
62
+ An acceptor is instantiated with the raw value and read through its
63
+ declared surface:
64
+
65
+ ```ruby
66
+ acceptor = Matsuri::Registry.acceptor('app').new(config)
67
+
68
+ acceptor.namespace # declared => the artifact's value
69
+ acceptor.log_level # not in config => 'info'
70
+ acceptor.owner # not declared => NoMethodError
71
+ ```
72
+
73
+ Note on defaults: the artifact usually arrives as a `Map`, and
74
+ `Map#fetch` does not honor Hash's default-value argument (it requires a
75
+ block). Use `options[:key] || default`.
76
+
77
+ ## Bare Classes
78
+
79
+ The registry definition is the golden path, not a requirement. Any class
80
+ that is instantiated with the raw value and answers the declared reads
81
+ is an acceptor:
82
+
83
+ ```ruby
84
+ class AppConfig
85
+ include Let
86
+ include RLet::LazyOptions
87
+
88
+ let(:namespace) { options[:namespace] }
89
+ end
90
+ ```
91
+
92
+ ## Built-in Acceptors
93
+
94
+ A stack registers acceptors under named collections with `accept`; last
95
+ declaration per collection wins. The collection name is the options key:
96
+ each acceptor admits its own component of the invocation options, whole,
97
+ and cannot see another collection's component. Whoever constructs the
98
+ stack supplies data under those keys.
99
+
100
+ `Matsuri::Stack` ships two built-ins:
101
+
102
+ ```ruby
103
+ accept :image_tag, Matsuri::Acceptors::ImageTag
104
+ accept :config, Matsuri::Acceptor
105
+ ```
106
+
107
+ The `:image_tag` collection carries a scalar. The converge positional
108
+ (`matsuri converge app v42`) arrives whole, and the acceptor's single
109
+ read, `image_tag`, owns the `latest` default — the only place that
110
+ default lives. With no positional, the acceptor admits the empty value
111
+ and the default answers.
112
+
113
+ The `:config` collection carries the stack-config artifact. Its built-in
114
+ is the empty acceptor: the collection is total — `stack_config` is the
115
+ same kind of value on every stack — and its surface is empty until a
116
+ platform re-declares the collection with its own acceptor.
117
+
118
+ A stack overrides a built-in by re-declaring its collection with its own
119
+ acceptor (registry name or class):
120
+
121
+ ```ruby
122
+ Matsuri.define :stack, 'app' do
123
+ accept :image_tag, 'pinned-image-tag'
124
+ accept :config, 'app'
125
+
126
+ needs :deployment, 'app-web'
127
+ end
128
+ ```
129
+
130
+ Downstream resources do not have to listen. Members that never read
131
+ `image_tag` are unaffected by what the acceptor admits, and the workload
132
+ classes themselves carry no default — a definition instantiated outside
133
+ a stack that wants one declares its own.
134
+
135
+ ## Acceptors and Stacks
136
+
137
+ Stacks are the first consumer. The stack never loads anything: the CLI
138
+ owns the boundary mapping between the user invocation and the stack's
139
+ promise, loading the file named by `--stack-config=` and supplying the
140
+ data under the `:config` key. Anything else that constructs a stack — a
141
+ spec, a controller, other tooling — supplies data the same way, with no
142
+ file involved. The stack binds its acceptor once per instance, and
143
+ resource members read the acceptor instance as `stack_config` — one
144
+ binding per converge, so members cannot disagree about what was
145
+ accepted. The pod template forwards the instance, so a pod definition
146
+ reads the same binding the owning resource holds. A missing flag is the
147
+ empty value: every declared read lands on its default. A flag with a bad
148
+ path fails loudly; there is no fallback. The stack wiring is described
149
+ in [stacks](stacks.md).
150
+
151
+ ## One Artifact, Many Consumers
152
+
153
+ An artifact usually carries vocabulary for more than one consumer, and
154
+ an acceptor claims only its own reads. Consider a feature-environment
155
+ artifact — an engineer names the environment, and the provisioning side
156
+ derives the rest:
157
+
158
+ ```yaml
159
+ name: feat-checkout
160
+ namespace: feat-checkout
161
+ dns_name: feat-checkout.example.dev
162
+ expires_at: "2026-11-15"
163
+ contact: team-checkout
164
+ ```
165
+
166
+ The stack's acceptor declares the fields the stack coordinates on — the
167
+ namespace, and the dns name it needs to configure CORS:
168
+
169
+ ```ruby
170
+ Matsuri.define :acceptor, 'app' do
171
+ let(:namespace) { options[:namespace] }
172
+ let(:dns_name) { options[:dns_name] }
173
+ end
174
+ ```
175
+
176
+ `expires_at` and `contact` pass through this acceptor in silence. They
177
+ are not dead vocabulary — they are the coordination path between an
178
+ environment reaper and whoever provisions environments. The reaper does
179
+ not have to be a matsuri consumer at all; it reads the same artifact on
180
+ its own terms. Adding those fields required no change to this acceptor,
181
+ and the stack never sees them.
data/docs/boot.md ADDED
@@ -0,0 +1,101 @@
1
+ # Boot
2
+
3
+ This covers what happens between invoking `matsuri` and a command
4
+ running: how invocation works and where your own code enters. The repo
5
+ layout itself is in [Setup](setup.md); the constraints behind this
6
+ design — the promise layers and the properties they induce — are in
7
+ [docs/design/boot.md](design/boot.md).
8
+
9
+ ## Invocation
10
+
11
+ matsuri <environment> <verb> ...
12
+
13
+ Invoke from the repo base — where the Gemfile is, in the manner of
14
+ terraform or chef. The environment is the first positional argument and
15
+ has no other source: there is no environment variable and no
16
+ significance to the binary's name. What you typed is what runs.
17
+ [Setup](setup.md) covers how an environment name binds to a kubectl
18
+ context and namespace.
19
+
20
+ ## The Wrapper
21
+
22
+ The repo's `bin/matsuri` makes the Gemfile's version binding unavoidable
23
+ from any shell. Pin and exec is its entire job:
24
+
25
+ ```bash
26
+ #!/bin/bash -e
27
+ SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
28
+ export BUNDLE_GEMFILE="${SCRIPT_DIR}/../Gemfile"
29
+ exec bundle exec matsuri "$@"
30
+ ```
31
+
32
+ A per-environment entry point is argument injection in a sibling script,
33
+ not wrapper logic:
34
+
35
+ ```bash
36
+ #!/bin/bash -e
37
+ # bin/staging
38
+ exec "$(dirname "$0")/matsuri" staging "$@"
39
+ ```
40
+
41
+ ## The Boot Sequence
42
+
43
+ 1. **Discovery.** Walk up from the working directory to the nearest
44
+ `config/matsuri.rb`; that directory is the repo base. No repo found
45
+ is a fatal error before any command logic runs.
46
+ 2. **Load path.** The repo's `lib/` is prepended to the load path.
47
+ Everything after this point may require the repo's own code.
48
+ 3. **Environment.** The first positional argument, verbatim.
49
+ 4. **Configuration.** `config/matsuri.rb` is loaded for `Config`
50
+ overrides, then the platform config (`config/platform.rb`, plus a
51
+ repo-local `.platform.rb` if present) for environment definitions.
52
+ Configuration is declarative — defaults and definitions, no
53
+ requires.
54
+ 5. **Activation.** `config/initializer.rb` is required, if present.
55
+ This is the one site that loads code: plugins and your extension
56
+ library. It runs after configuration, so it may branch on the
57
+ environment or any resolved setting.
58
+ 6. **Dispatch.** The remaining arguments go to the CLI.
59
+
60
+ ## Your Extension Library
61
+
62
+ Name a module after your organization, give it a file in `lib/`, and
63
+ autoload your mixins from it:
64
+
65
+ ```ruby
66
+ # lib/yourco.rb
67
+ module Yourco
68
+ module Pods
69
+ autoload :Healthcheck, 'yourco/pods/healthcheck'
70
+ end
71
+ end
72
+ ```
73
+
74
+ ```ruby
75
+ # config/initializer.rb
76
+ require 'yourco'
77
+ ```
78
+
79
+ Because boot put `lib/` on the load path before the initializer runs,
80
+ this works the same under `bundle exec`, a bare gem install, or a
81
+ checkout. The repo needs no gemspec. The mixins themselves are ordinary
82
+ concerns carrying `let()` definitions — see [Resources](resources.md)
83
+ for how they compose into definitions.
84
+
85
+ ## Plugins
86
+
87
+ The Gemfile declares what exists; the initializer activates it:
88
+
89
+ ```ruby
90
+ # Gemfile
91
+ gem 'matsuri', '~> 0.3'
92
+ gem 'matsuri-traefik'
93
+ ```
94
+
95
+ ```ruby
96
+ # config/initializer.rb
97
+ require 'matsuri/traefik'
98
+ ```
99
+
100
+ A gem declared but never required costs nothing and breaks nothing —
101
+ adding it to the Gemfile is not yet a decision about boot.
@@ -0,0 +1,157 @@
1
+ # Acceptors Design
2
+
3
+ This describes the constraints and properties of the acceptor design.
4
+ This is intended to provide both a grammar on how acceptors can be used,
5
+ but also extended.
6
+
7
+ ## Framing
8
+
9
+ An acceptor maps the information from a different bounded context into
10
+ the bounded context of Matsuri -- K8S resources and Matsuri abstractions
11
+ such as `stacks`.
12
+
13
+ This unidirectional mapping deconstructs potentially complex, nested
14
+ data into a flat let() space. The acceptor provides a stable keyspace
15
+ which can be used within Matsuri resources. Acceptors are not intended
16
+ to keep the domain closed, and are typically used to coordinate across
17
+ promise layers. Coordination here are promises (intentions) crossing
18
+ between contexts: a declared read is one such crossing, demanded by the
19
+ consumer that declares it, arriving in the receiver's own terms.
20
+
21
+ ## Grammar
22
+
23
+ The whole grammar is two promises. An acceptor is instantiated with the
24
+ admitted value, whole. It answers exactly the reads it declares, each
25
+ one a named definition on a flat surface. Everything else in this
26
+ document is a constraint on those two promises and the properties the
27
+ constraint induces.
28
+
29
+ ## Constraint: the accepted surface is declared, never inferred
30
+
31
+ Every read on the surface is an explicit declaration. There is no
32
+ delegation, no method_missing, no fallback to the underlying value.
33
+
34
+ Properties:
35
+
36
+ - Failure at the boundary is loud and immediate. A read the surface does
37
+ not answer raises at the point of use, instead of a nil surfacing
38
+ later inside a rendered manifest.
39
+ - Consumers couple to an owned interface, never to the artifact's
40
+ schema. When upstream renames a field, the remap happens in the
41
+ acceptor, in one place, and no consumer moves.
42
+ - Consumption is auditable. The full set of reads a platform permits
43
+ against an artifact is the acceptor's declarations — one file, not a
44
+ search across every consumer.
45
+ - Moving a consumer is legible. Whether a different surface can host it
46
+ reduces to one question — does that surface answer these reads — and a
47
+ wrong answer fails loudly, at the cut.
48
+
49
+ ## Constraint: the value is taken whole and deconstructed once
50
+
51
+ An acceptor receives the entire value at instantiation. There is one
52
+ parse site; consumers receive the acceptor instance, never fragments of
53
+ the raw value.
54
+
55
+ Properties:
56
+
57
+ - Consumers cannot disagree. Every read in an instance derives from the
58
+ same binding, so consistency is structural rather than disciplined.
59
+ - The boundary has an address. Questions about what the artifact means
60
+ to this platform have exactly one place to be answered, and changes to
61
+ that meaning have exactly one place to happen.
62
+
63
+ ## Constraint: unconsumed vocabulary passes in silence
64
+
65
+ An acceptor reads the fields it declares and is silent about the rest.
66
+ The silence is directional and deliberate: silent toward the side that
67
+ owns the vocabulary, loud toward the side that owns the surface.
68
+
69
+ Properties:
70
+
71
+ - Upstream can grow without coordination. New fields in the artifact are
72
+ a non-event for every acceptor that does not read them; no release of
73
+ the platform is needed for the artifact to say more.
74
+ - One artifact serves many consumers. A field this acceptor passes over
75
+ may be another consumer's declared read — the artifact carries many
76
+ parties' coordination at once, each acceptor selecting its own. A
77
+ partial consumer that rejected unknown fields would claim the whole
78
+ vocabulary and break the sharing; enforcement of the full vocabulary
79
+ belongs to whatever system owns it, at its own boundary.
80
+
81
+ ## Constraint: every declared read carries its default
82
+
83
+ Defaults live on the surface, in the declaration of each read — never
84
+ scattered through consumers.
85
+
86
+ Properties:
87
+
88
+ - The empty value is valid. An acceptor over an empty artifact answers
89
+ every read with its default; emptiness is the identity, not an error
90
+ case.
91
+ - Sparse values are the normal case. An artifact overrides only what it
92
+ names and inherits everything else, so the cost of an instance config
93
+ scales with its difference from the defaults, not with the size of the
94
+ surface.
95
+ - Shared-tier referents are expressions, not machinery. "This reference
96
+ defaults to the standing instance" is written as the default of one
97
+ read, visible at the declaration, overridable by the artifact.
98
+
99
+ ## Constraint: flat scalars are the golden path, documented rather than enforced
100
+
101
+ The surface is intended to be granular scalar reads. Declarations that
102
+ return nested structures are not prevented.
103
+
104
+ Properties:
105
+
106
+ - On the flat surface, override composition stays monoidically lawful:
107
+ defaults are the identity, a sparse artifact is an overlay, and no
108
+ merge semantics are ever needed.
109
+ - What a nested return forfeits is named rather than policed: the
110
+ sparse-overlay property stops at that read, and deep-merging the
111
+ structure is where the loss compounds. The cost falls on the
112
+ definition that chose the structure, not on the rest of the surface.
113
+
114
+ ## Constraint: an acceptor does not know its boundary; the accepting site binds it
115
+
116
+ An acceptor does not know which boundary it stands at. The declaration
117
+ that accepts — a stack naming its config acceptor, or any future
118
+ accepting site — binds the acceptor at its boundary, under a named
119
+ collection; the class itself promises only the grammar above.
120
+
121
+ Properties:
122
+
123
+ - One noun serves every foreign admission. New accepting sites bind
124
+ existing machinery; they do not mint new types.
125
+ - Acceptors are testable in isolation: hand one a value, read the
126
+ surface. No stack, environment, or cluster is implied.
127
+ - The registry definition and a bare class are interchangeable, because
128
+ the contract is only the two promises of the grammar. The registry is
129
+ a convenience of definition, not part of the contract.
130
+
131
+ ## Constraint: acceptors stand only at foreign boundaries
132
+
133
+ An acceptor is warranted where the admitted value's vocabulary has an
134
+ independent owner and lifecycle. Values the platform passes to itself
135
+ are already deconstructed by their consumers' own definitions.
136
+
137
+ Properties:
138
+
139
+ - The pattern cannot sprawl into a general indirection layer. Where no
140
+ independent owner exists there is no churn to absorb and no remap to
141
+ localize — an acceptor there adds a layer and no property, and the
142
+ definition excludes it rather than a style rule.
143
+ - The discriminator is decidable. Whether a boundary deserves an
144
+ acceptor reduces to one question: who owns this vocabulary, and does
145
+ it change on a lifecycle that is not ours?
146
+
147
+ ## Vocabulary note
148
+
149
+ The noun names the receiving side's act, and the declaration site is its
150
+ only habitat: consumers read the accepted value under the collection's
151
+ own name, so "acceptor" appears exactly where the policy is declared and
152
+ nowhere downstream. The inbound posture — read only what you declare —
153
+ has an outbound mirror in manifest transforms, which write only what
154
+ they own; the two share a discipline of least contact at boundaries,
155
+ never machinery. Resemblance to presenters, adapters, or anti-corruption
156
+ layers in other ecosystems may be adjacent and carries none of their
157
+ promises.