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/setup.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Setup
|
|
2
|
+
|
|
3
|
+
## A Matsuri Repo
|
|
4
|
+
|
|
5
|
+
The minimal skeleton:
|
|
6
|
+
|
|
7
|
+
Gemfile # pins the matsuri version (and plugins)
|
|
8
|
+
bin/matsuri # wrapper: pins the bundle, nothing else
|
|
9
|
+
config/
|
|
10
|
+
matsuri.rb # marks the repo base; Config overrides
|
|
11
|
+
platform.rb # environment definitions
|
|
12
|
+
initializer.rb # loads plugins and your library
|
|
13
|
+
lib/ # your organization's extension library
|
|
14
|
+
platform/ # resource definitions
|
|
15
|
+
|
|
16
|
+
`config/matsuri.rb` is the marker: matsuri finds the repo by walking up
|
|
17
|
+
from the working directory until it sees one. A working example repo
|
|
18
|
+
lives in [tests/example](../tests/example). How the pieces load — the
|
|
19
|
+
wrapper, the environment argument, the initializer — is covered in
|
|
20
|
+
[Boot](boot.md).
|
|
21
|
+
|
|
22
|
+
## Environments and kubectl Contexts
|
|
23
|
+
|
|
24
|
+
Every kubectl command matsuri runs is scoped to a context and a
|
|
25
|
+
namespace:
|
|
26
|
+
|
|
27
|
+
kubectl --context=<context> --namespace=<namespace> ...
|
|
28
|
+
|
|
29
|
+
The namespace comes from the environment's platform config (default
|
|
30
|
+
`default`). The context comes from one of two modes.
|
|
31
|
+
|
|
32
|
+
## Default Mode: the Environment Name Is the Context Name
|
|
33
|
+
|
|
34
|
+
By default, matsuri assumes a kubeconfig context exists with the same
|
|
35
|
+
name as the environment: `matsuri staging converge myapp` runs
|
|
36
|
+
`kubectl --context=staging ...`. Cluster tools hand out longer names —
|
|
37
|
+
GKE contexts look like `gke_myproject_us-central1_staging` — so matsuri
|
|
38
|
+
provides a verb that binds the environment's name as an alias:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
matsuri staging kubectl setup
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This runs `kubectl config set-context staging` with the cluster, user,
|
|
45
|
+
and namespace drawn from the environment's platform config:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# config/platform.rb
|
|
49
|
+
require 'map'
|
|
50
|
+
|
|
51
|
+
staging Map.new(
|
|
52
|
+
namespace: 'myapp',
|
|
53
|
+
kube_cluster: 'gke_myproject_us-central1_staging',
|
|
54
|
+
user: 'gke_myproject_us-central1_staging'
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`kube_cluster` and `user` default to the environment name when not
|
|
59
|
+
given; `namespace` defaults to `default`.
|
|
60
|
+
|
|
61
|
+
Setup is a one-time step per environment, and it only creates the alias:
|
|
62
|
+
the cluster and user entries it points at must already exist in your
|
|
63
|
+
kubeconfig. Get them from your provider first — for example
|
|
64
|
+
`gcloud container clusters get-credentials`, or `minikube start` /
|
|
65
|
+
`k3d cluster create`, which write their own entries. Local tools often
|
|
66
|
+
already name their context something usable (`minikube`,
|
|
67
|
+
`k3d-mycluster`), in which case name your environment to match and skip
|
|
68
|
+
setup entirely.
|
|
69
|
+
|
|
70
|
+
## Mapped Mode: Point the Environment at an Existing Context
|
|
71
|
+
|
|
72
|
+
If you would rather not alias — letting `gcloud` own authentication and
|
|
73
|
+
context naming, for instance — set `map_env_to_kube_context` and name
|
|
74
|
+
the real context in platform config:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# config/matsuri.rb
|
|
78
|
+
map_env_to_kube_context true
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# config/platform.rb
|
|
83
|
+
staging Map.new(
|
|
84
|
+
namespace: 'myapp',
|
|
85
|
+
kube_context: 'gke_myproject_us-central1_staging'
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
With this set, every environment must define `kube_context`;
|
|
90
|
+
`kubectl setup` is unnecessary in this mode.
|
|
91
|
+
|
|
92
|
+
## Verifying
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
kubectl config get-contexts # the environment (or mapped) context exists
|
|
96
|
+
matsuri staging status pods # matsuri reaches the cluster through it
|
|
97
|
+
```
|
data/docs/ssa.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Server-Side Apply
|
|
2
|
+
|
|
3
|
+
Server-side delivery hands the merge to the apiserver: instead of the
|
|
4
|
+
client's three-way last-applied merge, the server tracks which manager
|
|
5
|
+
owns which field and merges by ownership. Choose it when an object's
|
|
6
|
+
fields are genuinely shared — a deployment operator rolls the image, an
|
|
7
|
+
HPA moves the count, an in-place autoscaler adjusts sizing — and the
|
|
8
|
+
repo should own only what it declares.
|
|
9
|
+
|
|
10
|
+
The posture: matsuri owns exactly the fields the definition declares and
|
|
11
|
+
cedes everything else. The declaration is the ownership record; matsuri
|
|
12
|
+
never reads live state to decide what to send. The constraints behind
|
|
13
|
+
this are in [docs/design/ssa.md](design/ssa.md).
|
|
14
|
+
|
|
15
|
+
## Enabling It
|
|
16
|
+
|
|
17
|
+
Server-side apply is one value of the `apply_via` delivery axis, per
|
|
18
|
+
resource:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
Matsuri.define :deployment, 'app-web' do
|
|
22
|
+
let(:apply_via) { :server_side_apply }
|
|
23
|
+
# ...
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Applies are delivered under the repo's field manager —
|
|
28
|
+
`Matsuri::Platform.field_manager`, default `matsuri` — overridable per
|
|
29
|
+
resource with `let(:field_manager)`. Delivery mode and presenter
|
|
30
|
+
pipeline are orthogonal: flipping a resource's `apply_via` never changes
|
|
31
|
+
which presenters run, so members of a stack can migrate individually.
|
|
32
|
+
|
|
33
|
+
## Declare Only What You Manage
|
|
34
|
+
|
|
35
|
+
The ongoing payload should carry exactly the fields the repo owns.
|
|
36
|
+
Fields another writer rolls are omitted, not read back: a deployment
|
|
37
|
+
whose image is delivered by an operator declares its containers by name
|
|
38
|
+
without an image, and one whose count an HPA moves declares
|
|
39
|
+
`let(:replicas) { nil }` (Deployment's spec compacts, so nil is clean
|
|
40
|
+
omission). Boot values for those fields ride `:on_create` seeds — see
|
|
41
|
+
[Seeds](presenters.md#seeds) — so the seeded create lands the object
|
|
42
|
+
whole and the seeds end owned by no one.
|
|
43
|
+
|
|
44
|
+
## Persistence Normal Form
|
|
45
|
+
|
|
46
|
+
The server does not persist empty values — it drops them on storage —
|
|
47
|
+
and inside an atomic struct like `.spec.selector`, a payload carrying an
|
|
48
|
+
empty member no longer equals the persisted value, which turns the
|
|
49
|
+
creation hand-off into a conflict. An empty collection is also an
|
|
50
|
+
ownership claim over a field the definition does not manage.
|
|
51
|
+
|
|
52
|
+
So a server-side resource's payloads declare in persistence normal form:
|
|
53
|
+
declare `prune_empty` on both intent regimes, after any seeds, on the
|
|
54
|
+
stack (or the resource):
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Matsuri.define :stack, 'app' do
|
|
58
|
+
needs :deployment, 'app-web'
|
|
59
|
+
needs :service, 'app-web'
|
|
60
|
+
|
|
61
|
+
present :on_create, match_deployments(seed_replicas(2))
|
|
62
|
+
present :on_create, match_deployments(seed_image('registry.example.com/app:boot'))
|
|
63
|
+
|
|
64
|
+
present :on_create, prune_empty
|
|
65
|
+
present :on_apply, prune_empty
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A field that must be held empty against other writers is a genuine
|
|
70
|
+
assertion, not an accident of rendering — carry it in the exception list
|
|
71
|
+
(`prune_empty(except: [...])`). The mechanics and the exception grammar
|
|
72
|
+
are in [Prune Empty](presenters.md#prune-empty).
|
|
73
|
+
|
|
74
|
+
## The First Converge
|
|
75
|
+
|
|
76
|
+
Creation under server-side delivery is a protocol, not a single verb,
|
|
77
|
+
and the delivery verbs run it for you: `apply` (and so converge) routes
|
|
78
|
+
a resource that is not yet live through it, `create` brings one into
|
|
79
|
+
existence through it, and `recreate` tears down and reseeds. Three
|
|
80
|
+
steps:
|
|
81
|
+
|
|
82
|
+
1. Create the seeded payload as `<field_manager>-seed`.
|
|
83
|
+
2. Deliver the declared payload with an ordinary server-side apply.
|
|
84
|
+
Equal values co-own, which is why normal form matters at this step.
|
|
85
|
+
3. Cede: atomically remove the seed owner's managedFields entry. The
|
|
86
|
+
declared fields end solely owned by the field manager; the seeds end
|
|
87
|
+
unowned, free for the first writer that wants them.
|
|
88
|
+
|
|
89
|
+
A first converge looks like:
|
|
90
|
+
|
|
91
|
+
```console
|
|
92
|
+
$ bin/matsuri staging converge app
|
|
93
|
+
deployment.apps/app-web created
|
|
94
|
+
deployment.apps/app-web serverside-applied
|
|
95
|
+
service/app-web created
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Every later apply is an ordinary server-side apply. `save_config` is
|
|
99
|
+
pinned false under this mode — a seeded last-applied is a prune trap for
|
|
100
|
+
any later client-side apply — so there is nothing to configure.
|
|
101
|
+
|
|
102
|
+
The protocol and its refuted alternatives are pinned by
|
|
103
|
+
`tests/bin/probe-ssa.sh`.
|
|
104
|
+
|
|
105
|
+
## Conflicts
|
|
106
|
+
|
|
107
|
+
A conflict means another manager owns a field this declaration sends:
|
|
108
|
+
|
|
109
|
+
error: Apply failed with 1 conflict: conflict with "some-operator"
|
|
110
|
+
using apps/v1: .spec.template.spec.containers[name="app"].image
|
|
111
|
+
|
|
112
|
+
Matsuri never passes `--force-conflicts`; forcing transfers ownership
|
|
113
|
+
and overwrites another party's work, which is a human judgment. Two
|
|
114
|
+
resolutions:
|
|
115
|
+
|
|
116
|
+
- **Cede the field.** If the other manager is its rightful owner, stop
|
|
117
|
+
declaring it: omit it from the definition (and seed it on create if
|
|
118
|
+
the object needs it at creation). This is the common case and lands as
|
|
119
|
+
an ordinary reviewed change.
|
|
120
|
+
- **Take the field.** If the repo is the rightful owner, take it once
|
|
121
|
+
with raw kubectl under the repo's manager —
|
|
122
|
+
`kubectl apply --server-side --force-conflicts --field-manager=matsuri -f -`
|
|
123
|
+
— then the next matsuri apply is ordinary.
|
|
124
|
+
|
|
125
|
+
`diff` for a server-side resource dry-runs under the same field manager
|
|
126
|
+
apply uses, so a conflict surfaces in the preview as the same loud error
|
|
127
|
+
delivery would hit.
|
|
128
|
+
|
|
129
|
+
## An Interrupted First Converge
|
|
130
|
+
|
|
131
|
+
A converge that dies mid-protocol strands an object still owned by
|
|
132
|
+
`<field_manager>-seed` — the cede never ran, so the seed owner holds its
|
|
133
|
+
fields forever and a retry conflicts with it. Recognize it by reading
|
|
134
|
+
ownership:
|
|
135
|
+
|
|
136
|
+
```console
|
|
137
|
+
$ kubectl get deployment app-web --show-managed-fields -o json \
|
|
138
|
+
| jq '[.metadata.managedFields[].manager]'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If `matsuri-seed` appears on an object whose converge is not currently
|
|
142
|
+
running, the recovery is to delete the object and reconverge — the
|
|
143
|
+
seeding protocol runs only at creation, so it must restart from a clean
|
|
144
|
+
create:
|
|
145
|
+
|
|
146
|
+
```console
|
|
147
|
+
$ kubectl delete deployment app-web
|
|
148
|
+
$ bin/matsuri staging converge app
|
|
149
|
+
```
|
data/docs/stacks.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Stacks
|
|
2
|
+
|
|
3
|
+
Stacks are groupings of resources that are managed together.
|
|
4
|
+
|
|
5
|
+
## Defining Stacks
|
|
6
|
+
|
|
7
|
+
Stacks are defined in `platform/stacks` with a DSL that defines
|
|
8
|
+
resources that are needed. It takes an argument that references a
|
|
9
|
+
Matsuri definition and type.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# platform/stacks/app.rb
|
|
13
|
+
#
|
|
14
|
+
# One management promise: the app's bulkheads, the service fronting
|
|
15
|
+
# them, and the settings they share converge as one act, in this
|
|
16
|
+
# sequence.
|
|
17
|
+
Matsuri.define :stack, 'app' do
|
|
18
|
+
needs :config_map, 'app-settings'
|
|
19
|
+
needs :deployment, 'app-web'
|
|
20
|
+
needs :deployment, 'app-web-streams'
|
|
21
|
+
needs :deployment, 'app-worker-general'
|
|
22
|
+
needs :service, 'app-web'
|
|
23
|
+
end
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Each member stays independently addressable through the registry — the
|
|
27
|
+
stack adds sequence and shared bindings, not a new kind of resource.
|
|
28
|
+
|
|
29
|
+
At this time, there is no way to define dependencies. Applying the
|
|
30
|
+
manifest is performed in sequence.
|
|
31
|
+
|
|
32
|
+
## Operation
|
|
33
|
+
|
|
34
|
+
matsuri <env> converge <stack name>
|
|
35
|
+
|
|
36
|
+
The converge operation applies changes in sequence as defined by the
|
|
37
|
+
stack.
|
|
38
|
+
|
|
39
|
+
## Stack Config
|
|
40
|
+
|
|
41
|
+
We can pass a configuration yaml file into the stack that can be
|
|
42
|
+
referenced by resources. This is intended to parameterize an entire
|
|
43
|
+
stack. The main use-case is to support dynamic environments used within
|
|
44
|
+
software development lifecycle.
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
# stack-config.yaml
|
|
48
|
+
name: app-feat-checkout
|
|
49
|
+
namespace: feat-checkout
|
|
50
|
+
log_level: debug
|
|
51
|
+
expires_at: "2026-11-15"
|
|
52
|
+
contact: team-checkout
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The stack declares which acceptor admits the artifact with
|
|
56
|
+
`accept :config`; the acceptor owns the vocabulary and the defaults (see
|
|
57
|
+
[Acceptors](acceptors.md)):
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# platform/acceptors/app.rb
|
|
61
|
+
Matsuri.define :acceptor, 'app' do
|
|
62
|
+
let(:artifact) { options || {} }
|
|
63
|
+
|
|
64
|
+
let(:instance_name) { artifact[:name] }
|
|
65
|
+
let(:namespace) { artifact[:namespace] }
|
|
66
|
+
let(:log_level) { artifact[:log_level] || 'info' }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# platform/stacks/app.rb
|
|
70
|
+
Matsuri.define :stack, 'app' do
|
|
71
|
+
accept :config, 'app'
|
|
72
|
+
|
|
73
|
+
needs :config_map, 'app-settings'
|
|
74
|
+
needs :deployment, 'app-web'
|
|
75
|
+
needs :service, 'app-web'
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Every member resource reads the same binding through `stack_config`:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# platform/config_maps/app-settings.rb
|
|
83
|
+
Matsuri.define :config_map, 'app-settings' do
|
|
84
|
+
let(:data) do
|
|
85
|
+
{ 'LOG_LEVEL' => stack_config.log_level }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The artifact rides in on the converge:
|
|
91
|
+
|
|
92
|
+
matsuri <env> converge app --stack-config stack-config.yaml
|
|
93
|
+
|
|
94
|
+
Without the flag, every read lands on the acceptor's defaults — the
|
|
95
|
+
unconfigured stack behaves exactly as it did before the config surface
|
|
96
|
+
existed. Stack-config never crosses a stack boundary: a nested stack
|
|
97
|
+
reads only its own declarations.
|
|
98
|
+
|
|
99
|
+
## Server Side Apply
|
|
100
|
+
|
|
101
|
+
Stacks and resources can optionally use server side apply, in
|
|
102
|
+
combination with seeds.
|
|
103
|
+
|
|
104
|
+
We can also define seed presenters for stacks and resources on the
|
|
105
|
+
initial create. When combined with server side apply, Matsuri cedes
|
|
106
|
+
field management of those values. These presenters are not applied when
|
|
107
|
+
applying after resources are created. This allows other operators to
|
|
108
|
+
claim field management, while matsuri retains management over the
|
|
109
|
+
remaining fields.
|
|
110
|
+
|
|
111
|
+
Examples include seeded images (for deployment operators), replicas
|
|
112
|
+
(horizontal pod autoscaler), and resources (in-place pod autoscaling).
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# platform/deployments/app-web.rb
|
|
116
|
+
Matsuri.define :deployment, 'app-web' do
|
|
117
|
+
let(:apply_via) { :server_side_apply } # deliver by field ownership
|
|
118
|
+
let(:replicas) { nil } # omitted: the autoscaler owns the count
|
|
119
|
+
let(:match_labels) { { 'app' => 'web' } }
|
|
120
|
+
let(:pod_name) { 'app' } # pod 'app' carries no image —
|
|
121
|
+
# a deployment operator owns the tag
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# platform/stacks/app.rb
|
|
125
|
+
Matsuri.define :stack, 'app' do
|
|
126
|
+
needs :config_map, 'app-settings'
|
|
127
|
+
needs :deployment, 'app-web'
|
|
128
|
+
needs :service, 'app-web'
|
|
129
|
+
|
|
130
|
+
present :on_create, match_deployments(seed_replicas(2))
|
|
131
|
+
present :on_create, match_deployments(seed_image('registry.example.com/app:boot'))
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The first converge of a server-side resource routes through the seeding
|
|
136
|
+
protocol (seeded create, declared apply, cede); every later apply is an
|
|
137
|
+
ordinary server-side apply. Converge is sequential, so a member later in
|
|
138
|
+
the stack converges only after an earlier member is fully seeded and
|
|
139
|
+
ceded. The operator walkthrough — declaring only what you manage,
|
|
140
|
+
conflicts, interrupted converges — is [Server-Side Apply](ssa.md); the
|
|
141
|
+
full model of seeds and field managers is in the Seeds section of
|
|
142
|
+
[Presenters](presenters.md).
|
|
143
|
+
|
|
144
|
+
See also the seed and delivery specs:
|
|
145
|
+
[spec/presenters/seeds_spec.rb](../spec/presenters/seeds_spec.rb) and
|
|
146
|
+
[spec/tasks/stacks/seed_spec.rb](../spec/tasks/stacks/seed_spec.rb).
|
data/flake.lock
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nodes": {
|
|
3
|
+
"flake-utils": {
|
|
4
|
+
"inputs": {
|
|
5
|
+
"systems": "systems"
|
|
6
|
+
},
|
|
7
|
+
"locked": {
|
|
8
|
+
"lastModified": 1791914995,
|
|
9
|
+
"narHash": "sha256-Zf/xfQEAasnwnmEfHqW/4hjDE/s44yn2hD4RuVIZRjA=",
|
|
10
|
+
"owner": "numtide",
|
|
11
|
+
"repo": "flake-utils",
|
|
12
|
+
"rev": "b7a79d3fea1e214f6d668d521cecec0c01e26544",
|
|
13
|
+
"type": "github"
|
|
14
|
+
},
|
|
15
|
+
"original": {
|
|
16
|
+
"owner": "numtide",
|
|
17
|
+
"repo": "flake-utils",
|
|
18
|
+
"type": "github"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"nixpkgs": {
|
|
22
|
+
"locked": {
|
|
23
|
+
"lastModified": 1792615934,
|
|
24
|
+
"narHash": "sha256-MlvRfa1O5Cqc2/a+gG+l9TSuowEoCKCmrJ1d9oAVm0I=",
|
|
25
|
+
"owner": "NixOS",
|
|
26
|
+
"repo": "nixpkgs",
|
|
27
|
+
"rev": "da6dc0e7b26060f2f71ce3ef7088fb56ee44fa36",
|
|
28
|
+
"type": "github"
|
|
29
|
+
},
|
|
30
|
+
"original": {
|
|
31
|
+
"owner": "NixOS",
|
|
32
|
+
"ref": "nixos-26.05",
|
|
33
|
+
"repo": "nixpkgs",
|
|
34
|
+
"type": "github"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"root": {
|
|
38
|
+
"inputs": {
|
|
39
|
+
"flake-utils": "flake-utils",
|
|
40
|
+
"nixpkgs": "nixpkgs"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"systems": {
|
|
44
|
+
"locked": {
|
|
45
|
+
"lastModified": 1681028828,
|
|
46
|
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
47
|
+
"owner": "nix-systems",
|
|
48
|
+
"repo": "default",
|
|
49
|
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
50
|
+
"type": "github"
|
|
51
|
+
},
|
|
52
|
+
"original": {
|
|
53
|
+
"owner": "nix-systems",
|
|
54
|
+
"repo": "default",
|
|
55
|
+
"type": "github"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"root": "root",
|
|
60
|
+
"version": 7
|
|
61
|
+
}
|
data/flake.nix
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
description = "Matsuri dev environment";
|
|
3
|
+
|
|
4
|
+
inputs = {
|
|
5
|
+
nixpkgs = {
|
|
6
|
+
url = "github:NixOS/nixpkgs/nixos-26.05"; # Or a specific stable channel like "nixos-24.05"
|
|
7
|
+
};
|
|
8
|
+
flake-utils = {
|
|
9
|
+
url = "github:numtide/flake-utils";
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
outputs = { self, nixpkgs, flake-utils }:
|
|
14
|
+
flake-utils.lib.eachDefaultSystem(system:
|
|
15
|
+
let
|
|
16
|
+
pkgs = import nixpkgs {
|
|
17
|
+
inherit system;
|
|
18
|
+
# overlays = [ ];
|
|
19
|
+
};
|
|
20
|
+
in
|
|
21
|
+
with pkgs; {
|
|
22
|
+
devShell = mkShell {
|
|
23
|
+
buildInputs = [
|
|
24
|
+
ruby_4_0
|
|
25
|
+
pandoc
|
|
26
|
+
|
|
27
|
+
# Cluster harness for tests/ (delivery semantics probes)
|
|
28
|
+
minikube
|
|
29
|
+
kubectl
|
|
30
|
+
] ++ lib.optionals stdenv.isDarwin [
|
|
31
|
+
vfkit # hypervisor backend for minikube on macOS
|
|
32
|
+
];
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rlet'
|
|
4
|
+
require 'rlet/lazy_options'
|
|
5
|
+
|
|
6
|
+
module Matsuri
|
|
7
|
+
# An acceptor admits a value whose vocabulary matsuri does not own, such as
|
|
8
|
+
# a stack-config artifact. The raw value arrives whole as options; the lets
|
|
9
|
+
# declared here are the accepted surface, and members read the acceptor
|
|
10
|
+
# instead of the raw value. Fields the acceptor does not declare are
|
|
11
|
+
# ignored. Reads the acceptor does not declare raise NoMethodError. There
|
|
12
|
+
# is no fallback to the underlying value.
|
|
13
|
+
#
|
|
14
|
+
# The empty value belongs to the acceptor: every declared read is total
|
|
15
|
+
# over nil, because what nil means is collection-specific. A scalar
|
|
16
|
+
# surface reads options whole (options || default); a hash-shaped surface
|
|
17
|
+
# anchors once with let(:artifact) { options || {} } and reads fields off
|
|
18
|
+
# the anchor.
|
|
19
|
+
class Acceptor
|
|
20
|
+
include Let
|
|
21
|
+
include RLet::LazyOptions
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
def load_path
|
|
25
|
+
Matsuri::Config.acceptors_path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def definition_module_name
|
|
29
|
+
'Acceptors'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'matsuri/acceptor'
|
|
4
|
+
|
|
5
|
+
module Matsuri
|
|
6
|
+
module Acceptors
|
|
7
|
+
# Built-in acceptor for the :image_tag collection on stacks. Maps the
|
|
8
|
+
# converge positional — a scalar, admitted whole — onto the declared
|
|
9
|
+
# surface. The 'latest' default lives here and nowhere else; a platform
|
|
10
|
+
# overrides it by re-declaring the collection with its own acceptor.
|
|
11
|
+
class ImageTag < Matsuri::Acceptor
|
|
12
|
+
let(:image_tag) { options || 'latest' }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/matsuri/cmd.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'map'
|
|
4
|
+
require 'rlet'
|
|
5
|
+
require 'yaml'
|
|
6
|
+
require 'matsuri/core_ext'
|
|
7
|
+
|
|
8
|
+
module Matsuri
|
|
9
|
+
module Cmd
|
|
10
|
+
extend Concern
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def with_config(opts = {})
|
|
16
|
+
_final_options = final_options(opts)
|
|
17
|
+
|
|
18
|
+
Matsuri::Config.apply_configuration(_final_options)
|
|
19
|
+
|
|
20
|
+
yield _final_options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def final_options(opts = {})
|
|
24
|
+
options.merge(parent_options || {}).merge(opts).deep_symbolize_keys
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The CLI owns the boundary mapping; the built-in ImageTag acceptor
|
|
28
|
+
# owns the default — the same single owner the stack :image_tag
|
|
29
|
+
# collection uses. The positional may be nil.
|
|
30
|
+
def image_tag_for(positional)
|
|
31
|
+
Matsuri::Acceptors::ImageTag.new(positional).image_tag
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The CLI owns the boundary mapping: --stack-config= names a path,
|
|
35
|
+
# the stack's :config collection expects data. No flag is the empty
|
|
36
|
+
# value; a flag with a bad path fails loudly, never falls back.
|
|
37
|
+
def stack_config_data(opt)
|
|
38
|
+
path = opt[:stack_config]
|
|
39
|
+
return nil unless path
|
|
40
|
+
|
|
41
|
+
Matsuri.log(:fatal, "stack-config not found: #{path}") unless File.file?(path)
|
|
42
|
+
Map.new(YAML.safe_load_file(path, aliases: true))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'matsuri/core_ext'
|
|
4
|
+
|
|
5
|
+
module Matsuri
|
|
6
|
+
module Cmds
|
|
7
|
+
class Apply < Thor
|
|
8
|
+
include Matsuri::Cmd
|
|
9
|
+
|
|
10
|
+
def self.apply_cmd_for(resource_name, image_tag: false)
|
|
11
|
+
unless image_tag
|
|
12
|
+
define_method(resource_name) do |name = :not_specified|
|
|
13
|
+
apply_resource { Matsuri::Registry.fetch_or_load(resource_name, name).new }
|
|
14
|
+
end
|
|
15
|
+
else
|
|
16
|
+
define_method(resource_name) do |name = :not_specified, image_tag = nil|
|
|
17
|
+
apply_resource { Matsuri::Registry.fetch_or_load(resource_name, name).new(image_tag: image_tag_for(image_tag)) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc 'imported_manifest NAME', 'apply changes to an imported manifest'
|
|
23
|
+
apply_cmd_for :imported_manifest
|
|
24
|
+
|
|
25
|
+
desc 'pod POD_NAME', 'apply changes to a pod'
|
|
26
|
+
apply_cmd_for :pod, image_tag: true
|
|
27
|
+
|
|
28
|
+
desc 'replica-set REPLICA-SET_NAME', 'apply changes to a replica-set'
|
|
29
|
+
apply_cmd_for :replica_set, image_tag: true
|
|
30
|
+
map replicaset: :replica_set
|
|
31
|
+
map rs: :replica_set
|
|
32
|
+
|
|
33
|
+
desc 'stateful-set STATEFUL_SET_NAME', 'apply changes for stateful_set'
|
|
34
|
+
apply_cmd_for :stateful_set, image_tag: true
|
|
35
|
+
map sts: :stateful_set
|
|
36
|
+
|
|
37
|
+
desc 'daemon-set DAEMON_SET_NAME', 'apply changes for daemon_set'
|
|
38
|
+
apply_cmd_for :daemon_set, image_tag: true
|
|
39
|
+
map ds: :daemon_set
|
|
40
|
+
|
|
41
|
+
desc 'deployment DEPLOYMENT_NAME', 'apply changes to a deployment'
|
|
42
|
+
apply_cmd_for :deployment, image_tag: true
|
|
43
|
+
map deploy: :deployment
|
|
44
|
+
|
|
45
|
+
desc 'cron-job CRONJOB_NAME', 'apply changes to a cronjob'
|
|
46
|
+
apply_cmd_for :cron_job, image_tag: true
|
|
47
|
+
map cj: :cron_job
|
|
48
|
+
map cronjob: :cron_job
|
|
49
|
+
|
|
50
|
+
desc 'service SERVICE_NAME', 'apply changes to a service'
|
|
51
|
+
apply_cmd_for :service
|
|
52
|
+
|
|
53
|
+
desc 'ingress INGRESS_NAME', 'apply changes to a ingress'
|
|
54
|
+
apply_cmd_for :ingress
|
|
55
|
+
|
|
56
|
+
desc 'secret SECRET_NAME', 'apply changes to a secret'
|
|
57
|
+
apply_cmd_for :secret
|
|
58
|
+
|
|
59
|
+
desc 'config-map CONFIG_MAP_NAME', 'apply changes to a config map'
|
|
60
|
+
apply_cmd_for :config_map, image_tag: true
|
|
61
|
+
map configmap: :config_map
|
|
62
|
+
map cm: :config_map
|
|
63
|
+
|
|
64
|
+
desc 'horizontal_pod_autoscaler HPA_NAME', 'apply changes to a horizontal pod autoscaler'
|
|
65
|
+
apply_cmd_for :horizontal_pod_autoscaler
|
|
66
|
+
map hpa: :horizontal_pod_autoscaler
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def apply_resource
|
|
71
|
+
with_config do |opt|
|
|
72
|
+
resource = yield
|
|
73
|
+
Matsuri::Tasks::KubectlOps.apply(resource)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|