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/docs/local-dev.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Local Development
|
|
2
|
+
|
|
3
|
+
Matsuri grew up as a local development harness: build an image, point
|
|
4
|
+
the pod at your working tree, converge, and iterate. These workflows are
|
|
5
|
+
still in the gem. This page documents them as they exist, along with
|
|
6
|
+
where the ecosystem has since produced better tools.
|
|
7
|
+
|
|
8
|
+
## Docker Build and Push
|
|
9
|
+
|
|
10
|
+
A stack can own its image lifecycle. The `build` and `push` verbs drive
|
|
11
|
+
hooks that the stack definition supplies — the gem provides the wiring,
|
|
12
|
+
the platform provides the meaning:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
matsuri dev build myapp --version v42 --branch main
|
|
16
|
+
matsuri dev push myapp --version v42
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
Matsuri.define :stack, 'myapp' do
|
|
21
|
+
def build!(opt = {})
|
|
22
|
+
docker! "build -t myapp:#{opt[:version] || 'latest'} #{config.docker_path}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def push!(opt = {})
|
|
26
|
+
docker! "push myapp:#{opt[:version] || 'latest'}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Without a definition, both hooks fail with a message saying so; there is
|
|
32
|
+
no default build behavior. `build` accepts `--dev`, `--version`,
|
|
33
|
+
`--branch`, `--github-user`, and `--repo`; `push` accepts `--dev` and
|
|
34
|
+
`--version`. What the options mean is up to the hook.
|
|
35
|
+
|
|
36
|
+
Conventional paths for build inputs live in `Matsuri::Config`:
|
|
37
|
+
|
|
38
|
+
- `docker_path` (`docker/`) — Dockerfiles and build contexts
|
|
39
|
+
- `build_path` (`build/`) — build output
|
|
40
|
+
- `images_path` (`platform/images`) — image definitions
|
|
41
|
+
|
|
42
|
+
## Live Mounts
|
|
43
|
+
|
|
44
|
+
The iterate-without-rebuilding loop: mount the working tree into the pod
|
|
45
|
+
with a hostPath volume, so edits on the host appear in the running
|
|
46
|
+
container. Pod definitions get path helpers that resolve against the
|
|
47
|
+
platform repo:
|
|
48
|
+
|
|
49
|
+
- `src_path('myapp')` — `src/myapp` under the repo; the working tree
|
|
50
|
+
- `cache_path('bundle')` — `.cache/bundle`; persistent caches for gems,
|
|
51
|
+
node_modules, and other artifacts that should outlive the pod
|
|
52
|
+
- `base_path('etc/myapp.conf')` — any path under the repo
|
|
53
|
+
- `host_path_volume(name, path)` — the hostPath volume entry
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
Matsuri.define :pod, 'myapp-dev' do
|
|
57
|
+
let(:volumes) do
|
|
58
|
+
[
|
|
59
|
+
host_path_volume('src', src_path('myapp')),
|
|
60
|
+
host_path_volume('gems', cache_path('bundle'))
|
|
61
|
+
]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
let(:container) do
|
|
65
|
+
{
|
|
66
|
+
name: 'myapp',
|
|
67
|
+
image: 'myapp:latest',
|
|
68
|
+
volumeMounts: [
|
|
69
|
+
{ name: 'src', mountPath: '/app' },
|
|
70
|
+
{ name: 'gems', mountPath: '/bundle' }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This pattern assumes the kubelet can reach the host filesystem — true
|
|
78
|
+
for single-node local clusters where the node is your machine (or a VM
|
|
79
|
+
with the repo mounted into it). On Docker Desktop for Mac, hostPath file
|
|
80
|
+
I/O crosses the VM boundary and has historically been slow enough to
|
|
81
|
+
defeat the purpose; Linux with a local cluster is where this loop
|
|
82
|
+
shines.
|
|
83
|
+
|
|
84
|
+
## Interactive Verbs: sh, console, status
|
|
85
|
+
|
|
86
|
+
> **Deprecation notice**: [k9s](https://k9scli.io/) is a better tool for
|
|
87
|
+
> all of this — live resource views, shell into any container, logs,
|
|
88
|
+
> port-forwards, and resource metrics in one TUI, without any stack
|
|
89
|
+
> definition support. These verbs may be deprecated in a future release.
|
|
90
|
+
|
|
91
|
+
`sh` and `console` shell into a stack's primary workload. Like `build!`,
|
|
92
|
+
they drive hooks the stack definition supplies — `sh!(opt, args)` and
|
|
93
|
+
`console!(opt, args)` — and fail with a message when undefined:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
matsuri dev sh myapp # shell, as the container user
|
|
97
|
+
matsuri dev sh myapp -p web-0 # pick a pod
|
|
98
|
+
matsuri dev sh! myapp # shell, as root
|
|
99
|
+
matsuri dev console myapp # app console (rails c, iex, etc.)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`sh` accepts `--root`/`-r`, `--user`/`-u`, and `--pod`/`-p`; `sh!` is
|
|
103
|
+
`sh` with root; `console` accepts `--root`, `--user`, and `--pod`. The
|
|
104
|
+
distinction is convention: `sh` promises a shell, `console` promises the
|
|
105
|
+
application's REPL — what command actually runs is the hook's call.
|
|
106
|
+
|
|
107
|
+
`status` reports on resources, with per-resource subcommands and two
|
|
108
|
+
kubectl-top wrappers:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
matsuri dev status stack myapp
|
|
112
|
+
matsuri dev status pod myapp-web
|
|
113
|
+
matsuri dev status pods # kubectl top pods
|
|
114
|
+
matsuri dev status nodes # kubectl top nodes
|
|
115
|
+
matsuri dev top pods # top is an alias for status
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Resource-level `status` calls the resource's `status!`, which a
|
|
119
|
+
definition may implement; the Stack base class requires the definition
|
|
120
|
+
to supply it.
|
|
121
|
+
|
|
122
|
+
## Deploy Verbs: reload, update, migrate
|
|
123
|
+
|
|
124
|
+
These drive stack-definition hooks the same way `build` and `push` do,
|
|
125
|
+
but they encode an application deploy lifecycle rather than an image
|
|
126
|
+
one:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
matsuri dev reload myapp # push config without recreating pods
|
|
130
|
+
matsuri dev update myapp v42 # migrate, then roll to VERSION
|
|
131
|
+
matsuri dev migrate myapp v42 # run migrations only
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`update` accepts `--skip-migrations`. The intended shape was for the
|
|
135
|
+
stack definition to supply `update!(version, opt)` as its own deploy
|
|
136
|
+
choreography — run database migrations, then roll pods to the new
|
|
137
|
+
version — with `migrate` and `reload` as the narrower pieces.
|
|
138
|
+
|
|
139
|
+
**These verbs have not been modernized to current Kubernetes flows.**
|
|
140
|
+
They predate Deployments: rolling to a version is now the manifest
|
|
141
|
+
path's job (`converge <stack> <image_tag>`), and migrations are better
|
|
142
|
+
expressed as invoking a Job (or a `:task` dependency in the stack's
|
|
143
|
+
build order) than as imperative hook choreography. The wiring also shows
|
|
144
|
+
its age: `migrate` routes to a `migrate!` hook that has no stub on the
|
|
145
|
+
Stack base class, so without a definition it fails with a raw
|
|
146
|
+
NoMethodError rather than a message; and the CLI invokes `reload!` with
|
|
147
|
+
only the options hash, so the hook's `version` parameter never receives
|
|
148
|
+
a version. Prefer `converge` and Job resources; these verbs remain for
|
|
149
|
+
stacks that already define the hooks.
|
|
150
|
+
|
|
151
|
+
## Incomplete Features
|
|
152
|
+
|
|
153
|
+
Pieces of the local-dev story that were started and never finished, or
|
|
154
|
+
whose finishing era passed. They are not wired to any CLI verb; they are
|
|
155
|
+
kept here so their intent is on the record.
|
|
156
|
+
|
|
157
|
+
- `matsuri <env> rebuild` — prints "Rebuild not implemented yet" and
|
|
158
|
+
exits 1. The intended loop was `build!` then `recreate!`, which
|
|
159
|
+
`Stack#rebuild!` still implements for anyone who wires it up.
|
|
160
|
+
- `Matsuri::Tasks::Docker#kill_all!` — the cluster reset button from
|
|
161
|
+
when the cluster was a pile of containers on your own machine: stop
|
|
162
|
+
and force-remove **every container on the Docker daemon**, then
|
|
163
|
+
re-converge from definitions. Indiscriminate against a modern shared
|
|
164
|
+
daemon; do not wire this up casually.
|
|
165
|
+
- `Matsuri::Tasks::Docker#fix_pts!` — prints (does not run)
|
|
166
|
+
`sudo mount -o remount,ptmxmode=666 /dev/pts`, the boot-ritual fix for
|
|
167
|
+
an early-Docker bug where restrictive devpts permissions broke PTY
|
|
168
|
+
allocation, so `docker exec -it` — and therefore `sh` and `console` —
|
|
169
|
+
could not open a terminal.
|
|
170
|
+
- `Matsuri::Tasks::Pod#build!` and `#list!` — empty method bodies; a
|
|
171
|
+
per-pod image build task that never grew content.
|
|
172
|
+
- `Matsuri::Config.host_osx?` — platform detection left over from the
|
|
173
|
+
boot2docker era; currently has no callers.
|
data/docs/presenters.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Presenters
|
|
2
|
+
|
|
3
|
+
A presenter is one manifest-shaped step in a delivery pipeline:
|
|
4
|
+
`Manifest -> Manifest`. Resources stay pure generators; a presenter
|
|
5
|
+
transforms the rendered manifest after generation, at the delivery
|
|
6
|
+
boundary, before the payload is serialized for kubectl.
|
|
7
|
+
|
|
8
|
+
The promises:
|
|
9
|
+
|
|
10
|
+
- Zero-config and context-free. Construction takes nothing; every
|
|
11
|
+
parameter a step needs is bound before `#call` ever runs.
|
|
12
|
+
- The input manifest is never mutated. `#call` is framework-owned: it
|
|
13
|
+
makes one deep copy and hands it to `#transform`, so the copy is a
|
|
14
|
+
constraint, not a convention an author can forget.
|
|
15
|
+
- The empty pipeline is the identity. A stack with no declarations sends
|
|
16
|
+
payloads byte-identical to its rendered manifests.
|
|
17
|
+
- The diff previews the delivery. One site builds the presented payload
|
|
18
|
+
for both `diff` and `apply`, so what diff shows is what apply sends —
|
|
19
|
+
through presenters, with no extra work. The client-side `--hash-diff`
|
|
20
|
+
reads drift against the presented declaration the same way.
|
|
21
|
+
|
|
22
|
+
## Defining Presenters
|
|
23
|
+
|
|
24
|
+
Presenters are defined in `platform/presenters`. Authors implement
|
|
25
|
+
`#transform` against a manifest that is already theirs to mutate, and
|
|
26
|
+
return it. The Kubernetes dialect (`set_env`, `set_label`,
|
|
27
|
+
`append_volume`, `update_in`, and friends from
|
|
28
|
+
`Matsuri::Transform::Kubernetes`) is bare vocabulary inside
|
|
29
|
+
`#transform`.
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
Matsuri.define :presenter, 'fluentd-sidecar' do
|
|
33
|
+
def transform(manifest)
|
|
34
|
+
update_in(manifest, [:spec, :template, :spec]) do |pod_spec|
|
|
35
|
+
append_volume(pod_spec, name: 'varlog', emptyDir: {})
|
|
36
|
+
append_to(pod_spec, [:containers],
|
|
37
|
+
name: 'fluentd',
|
|
38
|
+
image: 'fluentd:v1.16',
|
|
39
|
+
volumeMounts: [{ name: 'varlog', mountPath: '/var/log' }])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
As with acceptors, the registry definition is the golden path, not a
|
|
46
|
+
requirement: any subclass of `Matsuri::Presenter` works, and anything
|
|
47
|
+
`#call`-able can join a pipeline.
|
|
48
|
+
|
|
49
|
+
## Declaring Pipelines on Stacks
|
|
50
|
+
|
|
51
|
+
A stack declares its pipelines with `present`. Unlike `accept`, where a
|
|
52
|
+
collection names a slot and the last declaration wins, a scenario
|
|
53
|
+
accumulates: each `present` appends, and declaration order is member
|
|
54
|
+
order.
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Matsuri.define :stack, 'app' do
|
|
58
|
+
needs :config_map, 'app-settings'
|
|
59
|
+
needs :deployment, 'app-web'
|
|
60
|
+
needs :service, 'app-web'
|
|
61
|
+
|
|
62
|
+
present :on_apply, match_deployments('fluentd-sidecar')
|
|
63
|
+
present :on_create, 'team-label'
|
|
64
|
+
end
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Members may be presenter registry names, presenter classes, `Callable`
|
|
68
|
+
instances, or anything `#call`-able.
|
|
69
|
+
|
|
70
|
+
### Scenarios Are Intent Regimes
|
|
71
|
+
|
|
72
|
+
Scenario names do not name kubectl verbs. There are two regimes:
|
|
73
|
+
|
|
74
|
+
- `:on_create` — bringing a resource into existence.
|
|
75
|
+
- `:on_apply` — delivering declared state to something that exists.
|
|
76
|
+
|
|
77
|
+
Each delivery verb assembles the pipeline for the regime it operates in:
|
|
78
|
+
`create` and the create leg of `recreate` assemble `:on_create`;
|
|
79
|
+
`apply`, `replace`, and `diff` assemble `:on_apply`. Neither replace nor
|
|
80
|
+
server-side apply is a third regime — `apply_via` (`:apply`, `:replace`,
|
|
81
|
+
`:server_side_apply`) picks how the payload travels, and the scenario
|
|
82
|
+
picks what the payload says; the two axes stay orthogonal, so flipping a
|
|
83
|
+
resource's `apply_via` never changes which presenters run. Diff is not a
|
|
84
|
+
scenario either: its meaning is "what converge will send", so it
|
|
85
|
+
assembles `:on_apply` and the preview extends through presenters for
|
|
86
|
+
free.
|
|
87
|
+
|
|
88
|
+
Declarations never cross a stack boundary: a nested stack assembles from
|
|
89
|
+
its own declarations, the same rule stack-config follows.
|
|
90
|
+
|
|
91
|
+
## Declaring on Resources
|
|
92
|
+
|
|
93
|
+
A resource declares its own pipelines with the same `present` grammar;
|
|
94
|
+
the default is none. When a resource converges through a stack, the two
|
|
95
|
+
declaration sites merge into one pipeline: **resource members first,
|
|
96
|
+
stack members last**. These are not slot semantics — both run. Position
|
|
97
|
+
is about who sees the completed manifest: the resource's pipeline
|
|
98
|
+
completes its self-knowledge (closer to generation than to delivery),
|
|
99
|
+
and the stack's steps sit where the server's own mutating admission sits
|
|
100
|
+
relative to the client's manifest — they admit the finished product,
|
|
101
|
+
including whatever the resource's steps produced. Delivery-context
|
|
102
|
+
additions mostly commute and lose nothing by running late; a step that
|
|
103
|
+
enforces a boundary is only enforcement if it runs last.
|
|
104
|
+
|
|
105
|
+
Direct resource verbs (no stack context) assemble the resource's own
|
|
106
|
+
declarations alone, so a resource behaves identically converged directly
|
|
107
|
+
or through an undeclared stack.
|
|
108
|
+
|
|
109
|
+
## Selective Steps: Combinators
|
|
110
|
+
|
|
111
|
+
A stack pipeline runs across every member manifest. Combinators make a
|
|
112
|
+
step selective without the step knowing: `match` wraps an element and
|
|
113
|
+
applies it only to manifests meeting the declared
|
|
114
|
+
`{kind:, name:, namespace:}` criteria — the conjunction of what is
|
|
115
|
+
declared, the same addressing grammar imported-manifest lookup uses —
|
|
116
|
+
and is the identity otherwise. An unknown criteria key is an error, so a
|
|
117
|
+
typo cannot become match-all.
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
present :on_apply, match({ kind: 'Deployment', name: 'app-web' }, 'fluentd-sidecar')
|
|
121
|
+
present :on_apply, match_kind('CronJob', 'team-label')
|
|
122
|
+
present :on_apply, match_deployments('fluentd-sidecar')
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Combinators are machinery, not presenters: they are never registered or
|
|
126
|
+
named in the registry. Names cross the declaration boundary; machinery
|
|
127
|
+
does not.
|
|
128
|
+
|
|
129
|
+
## Parameterized Steps
|
|
130
|
+
|
|
131
|
+
Presenters take no configuration, and that is a feature: a step that
|
|
132
|
+
needs a value binds it before the pipeline runs, in one of three ways.
|
|
133
|
+
|
|
134
|
+
A definition fills a concern's declared hole with a `let`:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
Matsuri.define :presenter, 'app-image' do
|
|
138
|
+
include MyPlatform::SeedImage # declares let(:image) { fail ... }
|
|
139
|
+
let(:image) { "registry.example.com/app:#{Matsuri::Config.image_tag}" }
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A declaration closes over a one-off value in a labeled leaf:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
present :on_apply, Matsuri::Transform::Callable.new(label: 'pin-replicas') { |m|
|
|
147
|
+
m.set([:spec, :replicas], 3)
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Or a helper manufactures an anonymous definition — the named form minus
|
|
152
|
+
the registry entry, with the boundary copy riding along:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
def seed_image_presenter(image_value)
|
|
156
|
+
Class.new(Matsuri::Presenter) do
|
|
157
|
+
include MyPlatform::SeedImage
|
|
158
|
+
let(:image) { image_value }
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
present :on_create, match_deployments(seed_image_presenter('app:v12'))
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The leaf form receives the pipeline carrier directly — a `Map` — and no
|
|
166
|
+
boundary copy of its own, so prefer the presenter forms for anything
|
|
167
|
+
reused.
|
|
168
|
+
|
|
169
|
+
## Seeds
|
|
170
|
+
|
|
171
|
+
A seed completes a create payload with a field's boot value — the
|
|
172
|
+
replica count, the boot image, initial resource settings — without ever
|
|
173
|
+
contesting a declaration. The model:
|
|
174
|
+
|
|
175
|
+
- **Additive on create.** Seeds ride `:on_create` declarations on stacks
|
|
176
|
+
and resources; the ongoing `:on_apply` payload never carries them.
|
|
177
|
+
- **Don't-clobber is structural.** A field only seeds when the manifest
|
|
178
|
+
omits it. A workload that declares `spec.replicas` has opted out — the
|
|
179
|
+
seed yields, nothing overwrites. (Deployment's spec compacts, so
|
|
180
|
+
`let(:replicas) { nil }` is clean omission.)
|
|
181
|
+
- **Controllers take ownership uncontested.** Because the create runs
|
|
182
|
+
without writing the seeded fields into last-applied (`save_config`
|
|
183
|
+
governs this), ongoing applies of a manifest that omits them have
|
|
184
|
+
nothing to say about them — an HPA that scales is never fought. The
|
|
185
|
+
cluster side of this is pinned by `tests/bin/probe-save-config.sh`.
|
|
186
|
+
|
|
187
|
+
This is matsuri's whole answer to externally-owned fields — there is no
|
|
188
|
+
read-back. Matsuri never reads live cluster state to make a declaration
|
|
189
|
+
decision: a field someone else rolls (an image tag updated by a deploy
|
|
190
|
+
operator, replicas moved by an HPA) is omitted from the ongoing payload,
|
|
191
|
+
not read back and re-sent. Under client-side apply the omission
|
|
192
|
+
guarantees non-interference (the probe harness pins it); under an
|
|
193
|
+
ongoing deployment operator the field belongs to that operator's field
|
|
194
|
+
manager via server-side apply. If you deliver tags manually, name the
|
|
195
|
+
tag — the invocation positional or the stack's `:image_tag` acceptor is
|
|
196
|
+
the source, never the cluster.
|
|
197
|
+
|
|
198
|
+
Under server-side delivery (`apply_via :server_side_apply`), ongoing
|
|
199
|
+
applies merge by field ownership under the repo's field manager
|
|
200
|
+
(`Matsuri::Platform.field_manager`, default `matsuri`), and creation is
|
|
201
|
+
a protocol rather than a single verb (`Tasks::Stacks::Seed`): create the
|
|
202
|
+
seeded payload as `<field_manager>-seed` (the seed owner), deliver the
|
|
203
|
+
declared payload with an ordinary server-side apply, then atomically
|
|
204
|
+
remove the seed owner's managedFields entry. The field manager ends sole
|
|
205
|
+
owner of the declared fields and the seeds end unowned — a deliverer
|
|
206
|
+
rolling the image, or a controller scaling, takes them uncontested. The
|
|
207
|
+
order is not stylistic: a server-side apply against an object with empty
|
|
208
|
+
managedFields attributes every pre-existing field to a ghost manager
|
|
209
|
+
that never relinquishes. `tests/bin/probe-ssa.sh` pins the protocol and
|
|
210
|
+
the refuted alternatives. The operator-facing walkthrough — declaring
|
|
211
|
+
only what you manage, conflicts, interrupted converges — is [Server-Side
|
|
212
|
+
Apply](ssa.md).
|
|
213
|
+
|
|
214
|
+
The delivery verbs honor the protocol themselves: `apply` (and so
|
|
215
|
+
converge) routes a server-side resource that is not yet live through
|
|
216
|
+
`Seed`, `create` brings one into existence through it, and `recreate`
|
|
217
|
+
tears down and reseeds. Every later apply is an ordinary server-side
|
|
218
|
+
apply. Converge is sequential, so a stack member later in `build_order`
|
|
219
|
+
converges only after an earlier member is fully seeded and ceded — place
|
|
220
|
+
whatever hands a field to another manager after the workload it manages.
|
|
221
|
+
|
|
222
|
+
Three concerns ship, each a machinery + declared-hole pair:
|
|
223
|
+
`Matsuri::Presenters::SeedReplicas` (`let(:replicas)`), `SeedImage`
|
|
224
|
+
(`let(:image)`, seeding only containers without one), and
|
|
225
|
+
`SeedResources` (`let(:resources)`, likewise). One seed concern per
|
|
226
|
+
presenter — compose seeds in the pipeline, not by stacking includes.
|
|
227
|
+
Each has a declaration-surface constructor closing the hole over its
|
|
228
|
+
argument:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
Matsuri.define :stack, 'app' do
|
|
232
|
+
needs :deployment, 'app-web'
|
|
233
|
+
needs :deployment, 'app-worker'
|
|
234
|
+
|
|
235
|
+
present :on_create, match_deployments(seed_replicas(2))
|
|
236
|
+
present :on_create, match_deployments(seed_resources(
|
|
237
|
+
requests: { cpu: '500m', memory: '512Mi' }
|
|
238
|
+
))
|
|
239
|
+
present :on_create, match({ kind: 'Deployment', name: 'app-worker' },
|
|
240
|
+
seed_replicas(1))
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Seed values close at declaration time, so they must be literals or reads
|
|
245
|
+
of global state (`Matsuri::Platform`, environment config) — instance
|
|
246
|
+
values like the converge positional do not reach class-level
|
|
247
|
+
declarations.
|
|
248
|
+
|
|
249
|
+
### save_config
|
|
250
|
+
|
|
251
|
+
`save_config` feeds create/recreate's `--save-config` flag, consulted
|
|
252
|
+
only when ongoing delivery consults last-applied (`apply_via :apply`):
|
|
253
|
+
under `:replace` or `:server_side_apply` it is pinned false, because a
|
|
254
|
+
seeded last-applied is a prune trap for any later client-side apply. It
|
|
255
|
+
resolves most-specific-first: a definition's own `let(:save_config)`,
|
|
256
|
+
then the owning stack's `save_config false` declaration, then the
|
|
257
|
+
repo-wide `Matsuri::Platform.save_config` (default `true`). Like
|
|
258
|
+
stack-config and `present` declarations, a stack's policy never crosses
|
|
259
|
+
to nested stacks.
|
|
260
|
+
|
|
261
|
+
## Prune Empty
|
|
262
|
+
|
|
263
|
+
`Matsuri::Presenters::PruneEmpty` prunes empty values — `nil`, `{}`,
|
|
264
|
+
`[]` — from the payload, bottom-up (a parent emptied by pruning prunes
|
|
265
|
+
too). Empty strings survive: `""` is a value Kubernetes assigns meaning
|
|
266
|
+
to. Array elements are never dropped — removing elements changes list
|
|
267
|
+
semantics — only their interiors prune.
|
|
268
|
+
|
|
269
|
+
Under server-side apply this is more than hygiene. An empty collection
|
|
270
|
+
in a payload is an ownership claim over a field the definition does not
|
|
271
|
+
manage, and inside an atomic struct (`.spec.selector`) it diverges from
|
|
272
|
+
the server's persisted normal form — the server drops empty members on
|
|
273
|
+
storage — so the seeding protocol's equal-values co-ownership turns into
|
|
274
|
+
a loud conflict. A pruned payload declares in persistence normal form:
|
|
275
|
+
ceding a field and saying nothing are the same act.
|
|
276
|
+
|
|
277
|
+
Declare it on both intent regimes, after any seeds, so the create's echo
|
|
278
|
+
equals the declaration:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
present :on_create, prune_empty
|
|
282
|
+
present :on_apply, prune_empty
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The merge is not uniformly monoidal — for some fields an empty value is
|
|
286
|
+
a genuine assertion (an atomic field held empty against other writers).
|
|
287
|
+
`let(:exceptions)` carries those paths, each preserved even when empty;
|
|
288
|
+
it ships with none. Fill it through the helper or a named definition:
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
present :on_apply, prune_empty(except: [%i[spec selector matchExpressions]])
|
|
292
|
+
|
|
293
|
+
Matsuri.define :presenter, 'prune-empty' do
|
|
294
|
+
include Matsuri::Presenters::PruneEmpty
|
|
295
|
+
let(:exceptions) { [%i[spec selector matchExpressions]] }
|
|
296
|
+
end
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Paths address hash keys only; they do not descend into arrays.
|
|
300
|
+
|
|
301
|
+
## Composition
|
|
302
|
+
|
|
303
|
+
Pipelines are `Matsuri::Transform::Callable` values: `new` is the
|
|
304
|
+
identity, `new(label:) { }` a leaf, `new([a, b])` a composite, and `+`
|
|
305
|
+
concatenates. Composition obeys the monoid laws, so bundles nest without
|
|
306
|
+
surprises: a composite passed as a member stays one member, and its
|
|
307
|
+
label rides along as trace metadata.
|
|
308
|
+
|
|
309
|
+
## What Presenters Are Not
|
|
310
|
+
|
|
311
|
+
Presenters are not the place for a resource's own vocabulary — a
|
|
312
|
+
deployment that always wants a label declares it in its manifest.
|
|
313
|
+
Presenters carry what the *delivery context* adds: the stack-wide
|
|
314
|
+
sidecar, the environment stamp, the value seeded only at creation.
|
|
315
|
+
Generation stays in definitions, delivery policy in lets
|
|
316
|
+
(`converge_via`, `apply_via`, `save_config`), and presentation in the
|
|
317
|
+
pipeline between them.
|
data/docs/resources.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Resources
|
|
2
|
+
|
|
3
|
+
Resources are defined using `let()` extracted from `rspec` (shipped as
|
|
4
|
+
the `rlet` gem). Each resource has a `manifest` let that assembles the
|
|
5
|
+
complete resource manifest. Every value in it is constructed by a
|
|
6
|
+
predefined `let()` that you override with your own `let()`: the manifest
|
|
7
|
+
is a composition of small named values, and redefining one name changes
|
|
8
|
+
exactly that leaf. Values that have no sensible default — a deployment's
|
|
9
|
+
`replicas`, its `match_labels` — are declared holes that fail fast until
|
|
10
|
+
a definition fills them.
|
|
11
|
+
|
|
12
|
+
Resource definitions are Ruby classes, so inheritance can be used: a
|
|
13
|
+
definition can inherit from another definition and override only the
|
|
14
|
+
lets that differ.
|
|
15
|
+
|
|
16
|
+
You can create mixins with `Concern` that carry their own `let()`. This
|
|
17
|
+
allows you to include them across definitions. Matsuri comes with a few
|
|
18
|
+
builtin mixins:
|
|
19
|
+
|
|
20
|
+
- [`Concerns::Pods::SpreadTopology`](../lib/matsuri/concerns/pods/spread_topology.rb)
|
|
21
|
+
— topology spread constraint vocabulary for balancing same-labeled
|
|
22
|
+
pods across zones or nodes.
|
|
23
|
+
- [`Concerns::Pods::SpreadPods`](../lib/matsuri/concerns/pods/spread_pods.rb)
|
|
24
|
+
— pod anti-affinity vocabulary for excluding co-location within a node
|
|
25
|
+
or zone. Complements SpreadTopology: spread expresses balance,
|
|
26
|
+
anti-affinity expresses exclusion.
|
|
27
|
+
- [`Concerns::PodTemplate`](../lib/matsuri/concerns/pod_template.rb) —
|
|
28
|
+
embeds a pod definition as a workload's pod template; this is how
|
|
29
|
+
Deployment, StatefulSet, and the other workloads reference pods.
|
|
30
|
+
- [`Concerns::ImportManifest`](../lib/matsuri/concerns/import_manifest.rb)
|
|
31
|
+
— imports vendor YAML manifests as source material and transforms them
|
|
32
|
+
with the same algebra.
|
|
33
|
+
|
|
34
|
+
## Defining a Resource in the Registry
|
|
35
|
+
|
|
36
|
+
Definitions live under `platform/`, one directory per resource type, one
|
|
37
|
+
file per definition. The registry resolves `(type, name)` to the file by
|
|
38
|
+
convention — `Matsuri.define :pod, 'web'` is found in
|
|
39
|
+
`platform/pods/web.rb`:
|
|
40
|
+
|
|
41
|
+
platform/
|
|
42
|
+
acceptors/app.rb
|
|
43
|
+
config_maps/app-settings.rb
|
|
44
|
+
deployments/web.rb
|
|
45
|
+
pods/web.rb
|
|
46
|
+
presenters/team-label.rb
|
|
47
|
+
services/web.rb
|
|
48
|
+
stacks/app.rb
|
|
49
|
+
|
|
50
|
+
`Matsuri.define` registers the definition and returns the class, so a
|
|
51
|
+
file can also refine what it defines (or define bare classes and
|
|
52
|
+
register them directly — the registry is the golden path, not a
|
|
53
|
+
requirement).
|
|
54
|
+
|
|
55
|
+
### Environments
|
|
56
|
+
|
|
57
|
+
A definition can be specialized per environment by path: the registry
|
|
58
|
+
consults `platform/pods/<environment>/web.rb` before
|
|
59
|
+
`platform/pods/web.rb`, so an environment-specific file shadows the
|
|
60
|
+
shared one. The environment is the CLI's first argument
|
|
61
|
+
(`matsuri staging ...`); see [Setup](setup.md) for how environments bind
|
|
62
|
+
to kubectl contexts.
|
|
63
|
+
|
|
64
|
+
See also the rspec fixture platform, which is a small, complete example
|
|
65
|
+
repo: [spec/fixture/platform](../spec/fixture/platform).
|
|
66
|
+
|
|
67
|
+
## Examples
|
|
68
|
+
|
|
69
|
+
### Pods
|
|
70
|
+
|
|
71
|
+
The smallest definition overrides `spec` outright:
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# platform/pods/hello.rb
|
|
75
|
+
Matsuri.define :pod, 'hello' do
|
|
76
|
+
let(:spec) do
|
|
77
|
+
{ containers: [{ name: 'hello', image: 'nginx:1.25' }] }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Most definitions override narrower lets and let the predefined `spec`
|
|
83
|
+
assemble around them:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
# platform/pods/web.rb
|
|
87
|
+
Matsuri.define :pod, 'web' do
|
|
88
|
+
let(:container) { { name: 'web', image: "app:#{image_tag}" } }
|
|
89
|
+
let(:volumes) { [] }
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Placement vocabulary (`node_selector`, `tolerations`, `affinity`,
|
|
94
|
+
`topology_spread_constraints`) is predefined on Pod; the spread concerns
|
|
95
|
+
above supply strategies for the affinity and spread lets.
|
|
96
|
+
|
|
97
|
+
See also the pod fixtures:
|
|
98
|
+
[spec/fixture/platform/pods](../spec/fixture/platform/pods).
|
|
99
|
+
|
|
100
|
+
### Deployments
|
|
101
|
+
|
|
102
|
+
A deployment embeds a pod definition as its template by naming it —
|
|
103
|
+
`let(:pod_name)` is the reference, and the pod's labels, annotations,
|
|
104
|
+
and spec ride in through `Concerns::PodTemplate`:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# platform/deployments/web.rb
|
|
108
|
+
Matsuri.define :deployment, 'web' do
|
|
109
|
+
let(:replicas) { 2 }
|
|
110
|
+
let(:match_labels) { { 'app' => 'web' } }
|
|
111
|
+
let(:pod_name) { 'web' } # embeds pod 'web' as the pod template
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`replicas` and `match_labels` are declared holes — a deployment that
|
|
116
|
+
does not define them fails at first render, not at apply time. Rollout
|
|
117
|
+
knobs (`strategy`, `max_unavailable`, `max_surge`, `min_ready_seconds`)
|
|
118
|
+
are predefined lets with conventional defaults.
|
|
119
|
+
|
|
120
|
+
See also the deployment fixtures:
|
|
121
|
+
[spec/fixture/platform/deployments](../spec/fixture/platform/deployments).
|