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.
- checksums.yaml +5 -5
- data/.envrc +1 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +9 -0
- data/AGENTS.md +18 -0
- data/CHANGELOG.md +642 -0
- data/Gemfile +2 -0
- data/README.md +86 -8
- data/Rakefile +20 -0
- data/bin/console +1 -0
- data/bin/matsuri +6 -0
- data/bin/setup +2 -0
- data/docs/acceptors.md +181 -0
- data/docs/boot.md +101 -0
- data/docs/design/acceptors.md +157 -0
- data/docs/design/boot.md +131 -0
- data/docs/design/ssa.md +142 -0
- data/docs/design/stacks.md +153 -0
- data/docs/diff.md +85 -0
- data/docs/known-issues.md +19 -0
- data/docs/local-dev.md +173 -0
- data/docs/presenters.md +317 -0
- data/docs/resources.md +121 -0
- data/docs/setup.md +97 -0
- data/docs/ssa.md +149 -0
- data/docs/stacks.md +146 -0
- data/flake.lock +61 -0
- data/flake.nix +35 -0
- data/lib/matsuri/acceptor.rb +33 -0
- data/lib/matsuri/acceptors/image_tag.rb +15 -0
- data/lib/matsuri/cmd.rb +46 -0
- data/lib/matsuri/cmds/apply.rb +78 -0
- data/lib/matsuri/cmds/cli.rb +147 -0
- data/lib/matsuri/cmds/create.rb +87 -0
- data/lib/matsuri/cmds/delete.rb +86 -0
- data/lib/matsuri/cmds/diff.rb +105 -0
- data/lib/matsuri/cmds/generate.rb +24 -0
- data/lib/matsuri/cmds/kubectl.rb +16 -0
- data/lib/matsuri/cmds/recreate.rb +65 -0
- data/lib/matsuri/cmds/scale.rb +37 -0
- data/lib/matsuri/cmds/show.rb +123 -0
- data/lib/matsuri/cmds/status.rb +67 -0
- data/lib/matsuri/concerns/awaiting.rb +24 -0
- data/lib/matsuri/concerns/diffable.rb +135 -0
- data/lib/matsuri/concerns/export_manifest.rb +56 -0
- data/lib/matsuri/concerns/import_manifest.rb +64 -0
- data/lib/matsuri/concerns/pod_template.rb +37 -0
- data/lib/matsuri/concerns/pods/spread_pods.rb +47 -0
- data/lib/matsuri/concerns/pods/spread_topology.rb +63 -0
- data/lib/matsuri/concerns/registry_helpers.rb +59 -0
- data/lib/matsuri/config.rb +101 -0
- data/lib/matsuri/core_ext.rb +107 -0
- data/lib/matsuri/dsl/cluster/aggregated_cluster_role.rb +75 -0
- data/lib/matsuri/dsl/cluster/binding.rb +118 -0
- data/lib/matsuri/dsl/cluster/cluster_role.rb +41 -0
- data/lib/matsuri/dsl/cluster/role.rb +49 -0
- data/lib/matsuri/dsl/cluster/scope.rb +245 -0
- data/lib/matsuri/dsl/cluster/service_account.rb +76 -0
- data/lib/matsuri/dsl/concerns/default_binding.rb +34 -0
- data/lib/matsuri/dsl/concerns/manifest_set.rb +21 -0
- data/lib/matsuri/dsl/concerns/metadata.rb +56 -0
- data/lib/matsuri/dsl/concerns/rbac_rules.rb +39 -0
- data/lib/matsuri/initializer.rb +41 -0
- data/lib/matsuri/kubernetes/base.rb +107 -0
- data/lib/matsuri/kubernetes/config_map.rb +100 -0
- data/lib/matsuri/kubernetes/cron_job.rb +115 -0
- data/lib/matsuri/kubernetes/daemon_set.rb +62 -0
- data/lib/matsuri/kubernetes/deployment.rb +65 -0
- data/lib/matsuri/kubernetes/endpoints.rb +57 -0
- data/lib/matsuri/kubernetes/horizontal_pod_autoscaler.rb +116 -0
- data/lib/matsuri/kubernetes/imported_manifest.rb +61 -0
- data/lib/matsuri/kubernetes/ingress.rb +66 -0
- data/lib/matsuri/kubernetes/persistent_volume.rb +56 -0
- data/lib/matsuri/kubernetes/persistent_volume_claim.rb +41 -0
- data/lib/matsuri/kubernetes/pod.rb +355 -0
- data/lib/matsuri/kubernetes/replica_set.rb +46 -0
- data/lib/matsuri/kubernetes/secret.rb +86 -0
- data/lib/matsuri/kubernetes/service.rb +49 -0
- data/lib/matsuri/kubernetes/stateful_set.rb +75 -0
- data/lib/matsuri/kubernetes/storage_class.rb +45 -0
- data/lib/matsuri/platform.rb +38 -0
- data/lib/matsuri/presenter.rb +47 -0
- data/lib/matsuri/presenters/helpers.rb +39 -0
- data/lib/matsuri/presenters/prune_empty.rb +59 -0
- data/lib/matsuri/presenters/seed_image.rb +28 -0
- data/lib/matsuri/presenters/seed_replicas.rb +25 -0
- data/lib/matsuri/presenters/seed_resources.rb +29 -0
- data/lib/matsuri/registry.rb +267 -0
- data/lib/matsuri/shell_out.rb +119 -0
- data/lib/matsuri/stack.rb +276 -0
- data/lib/matsuri/task.rb +20 -0
- data/lib/matsuri/tasks/cluster.rb +79 -0
- data/lib/matsuri/tasks/docker.rb +25 -0
- data/lib/matsuri/tasks/hash_diff.rb +61 -0
- data/lib/matsuri/tasks/kubectl.rb +21 -0
- data/lib/matsuri/tasks/kubectl_ops/apply.rb +24 -0
- data/lib/matsuri/tasks/kubectl_ops/create.rb +27 -0
- data/lib/matsuri/tasks/kubectl_ops/delete.rb +21 -0
- data/lib/matsuri/tasks/kubectl_ops/diff.rb +47 -0
- data/lib/matsuri/tasks/kubectl_ops/get.rb +36 -0
- data/lib/matsuri/tasks/kubectl_ops/recreate.rb +35 -0
- data/lib/matsuri/tasks/kubectl_ops/replace.rb +29 -0
- data/lib/matsuri/tasks/kubectl_ops/rollout_status.rb +22 -0
- data/lib/matsuri/tasks/kubectl_ops/scale.rb +22 -0
- data/lib/matsuri/tasks/kubectl_ops/server_side_apply.rb +32 -0
- data/lib/matsuri/tasks/kubectl_ops/status.rb +20 -0
- data/lib/matsuri/tasks/kubectl_ops.rb +218 -0
- data/lib/matsuri/tasks/pod.rb +56 -0
- data/lib/matsuri/tasks/stacks/seed.rb +93 -0
- data/lib/matsuri/tasks/stacks.rb +31 -0
- data/lib/matsuri/transform/algebra.rb +52 -0
- data/lib/matsuri/transform/callable.rb +54 -0
- data/lib/matsuri/transform/combinators.rb +61 -0
- data/lib/matsuri/transform/env.rb +43 -0
- data/lib/matsuri/transform/kubernetes.rb +89 -0
- data/lib/matsuri/transform.rb +20 -0
- data/lib/matsuri/version.rb +3 -1
- data/lib/matsuri.rb +158 -2
- data/matsuri.gemspec +20 -7
- data/tests/bin/minikube-start.sh +27 -0
- data/tests/bin/probe-save-config.sh +46 -0
- data/tests/bin/probe-ssa.sh +206 -0
- data/tests/example/.gitignore +1 -0
- data/tests/example/Gemfile +7 -0
- data/tests/example/README.md +91 -0
- data/tests/example/bin/matsuri +14 -0
- data/tests/example/config/initializer.rb +12 -0
- data/tests/example/config/matsuri.rb +9 -0
- data/tests/example/config/platform.rb +39 -0
- data/tests/example/lib/acme/nodes/dedicated.rb +26 -0
- data/tests/example/lib/acme/nodes/preemptible.rb +17 -0
- data/tests/example/lib/acme/nodes/required_cores.rb +26 -0
- data/tests/example/lib/acme/nodes/resource_class.rb +26 -0
- data/tests/example/lib/acme/pods/cacerts.rb +15 -0
- data/tests/example/lib/acme/pods/healthcheck.rb +36 -0
- data/tests/example/lib/acme.rb +26 -0
- data/tests/example/platform/config_maps/billing-settings.rb +8 -0
- data/tests/example/platform/config_maps/cacerts.rb +9 -0
- data/tests/example/platform/deployments/billing-web.rb +5 -0
- data/tests/example/platform/deployments/checkout-web.rb +6 -0
- data/tests/example/platform/namespaces/billing-dev.yaml +7 -0
- data/tests/example/platform/namespaces/checkout-dev.yaml +7 -0
- data/tests/example/platform/pods/billing.rb +31 -0
- data/tests/example/platform/pods/checkout.rb +27 -0
- data/tests/example/platform/services/billing-web.rb +6 -0
- data/tests/example/platform/services/checkout-web.rb +4 -0
- data/tests/example/platform/stacks/billing.rb +8 -0
- data/tests/example/platform/stacks/checkout.rb +19 -0
- data/tests/k8s/save-config/deployment-declared.yaml +19 -0
- data/tests/k8s/save-config/deployment-seeded.yaml +20 -0
- data/tests/k8s/save-config/namespace.yaml +4 -0
- data/tests/k8s/ssa/declared-bumped.yaml +22 -0
- data/tests/k8s/ssa/declared.yaml +22 -0
- data/tests/k8s/ssa/namespace.yaml +4 -0
- data/tests/k8s/ssa/seed-via-create.yaml +24 -0
- metadata +288 -20
- data/.travis.yml +0 -4
data/README.md
CHANGED
|
@@ -1,8 +1,56 @@
|
|
|
1
1
|
# Matsuri
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
data/bin/matsuri
ADDED
data/bin/setup
CHANGED
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.
|