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/design/boot.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Boot Design
|
|
2
|
+
|
|
3
|
+
This describes the constraints and properties of the boot design: the
|
|
4
|
+
sequence that takes a process from an invoked binary to a command
|
|
5
|
+
running against an environment, with the repo's vocabulary loaded. This
|
|
6
|
+
is intended to provide both a grammar for how a Matsuri repo boots, and
|
|
7
|
+
for how the boot can be extended.
|
|
8
|
+
|
|
9
|
+
The promises mentioned in this document are understood as concepts from
|
|
10
|
+
Promise Theory.
|
|
11
|
+
|
|
12
|
+
## Framing
|
|
13
|
+
|
|
14
|
+
Boot is where several parties meet: the matsuri gem, the platform repo,
|
|
15
|
+
the shell that invoked the binary, and the operator or agent that typed
|
|
16
|
+
the command. Each party can make promises only about what it owns. The
|
|
17
|
+
design assigns each resolution question of boot to exactly one owner, as
|
|
18
|
+
layers: each layer makes one promise to the layers above it and assumes
|
|
19
|
+
only the layers below.
|
|
20
|
+
|
|
21
|
+
## Promises
|
|
22
|
+
|
|
23
|
+
Boot resolves five questions, in order, each with one owner:
|
|
24
|
+
|
|
25
|
+
1. Which matsuri runs — promised by the repo's Gemfile.
|
|
26
|
+
2. Where the repo is — discovered from the working directory.
|
|
27
|
+
3. What code the repo contributes — loaded by matsuri from the repo's
|
|
28
|
+
`lib/`.
|
|
29
|
+
4. Which environment is meant — named by the first positional argument.
|
|
30
|
+
5. What is activated — declared by the repo's initializer.
|
|
31
|
+
|
|
32
|
+
Then the command dispatches. Everything below is a constraint on these
|
|
33
|
+
ownerships and the properties each induces.
|
|
34
|
+
|
|
35
|
+
## Constraint: version binding belongs to the repo's Gemfile
|
|
36
|
+
|
|
37
|
+
The platform repo locks its matsuri version, and the versions of any
|
|
38
|
+
plugins, in its own Gemfile. Matsuri never resolves its own version, and
|
|
39
|
+
nothing in boot selects among installed versions. A repo that wants the
|
|
40
|
+
binding to be unavoidable ships a `bin/matsuri` wrapper whose whole job
|
|
41
|
+
is to pin `BUNDLE_GEMFILE` and exec `bundle exec matsuri`.
|
|
42
|
+
|
|
43
|
+
Properties:
|
|
44
|
+
|
|
45
|
+
- The matsuri a repo runs against is a recorded, reviewable promise.
|
|
46
|
+
Upgrades are a diff to the lockfile, per repo, on that repo's cadence.
|
|
47
|
+
- Two repos on one machine can promise different matsuri versions
|
|
48
|
+
without negotiation.
|
|
49
|
+
- The wrapper has no logic to drift. Pin and exec is the entire content;
|
|
50
|
+
everything else the old wrappers did is owned by a layer below.
|
|
51
|
+
|
|
52
|
+
## Constraint: discovery anchors at the working directory
|
|
53
|
+
|
|
54
|
+
Matsuri finds the repo by walking up from the working directory looking
|
|
55
|
+
for `config/matsuri.rb`. The contract of use is to invoke from the repo
|
|
56
|
+
base — where the Gemfile is, in the manner of terraform or chef — and
|
|
57
|
+
the walk-up exists as forgiveness for subdirectory invocation, not as a
|
|
58
|
+
second contract. No environment variable, registry, or wrapper location
|
|
59
|
+
tells matsuri where repos live; standing in the repo is the claim.
|
|
60
|
+
|
|
61
|
+
Properties:
|
|
62
|
+
|
|
63
|
+
- Repo identity and version binding coincide. Invoked at the repo base,
|
|
64
|
+
the repo that discovery finds is the repo whose Gemfile bound the gem;
|
|
65
|
+
there is no configuration by which they diverge.
|
|
66
|
+
- The invocation is portable. A command that works in one checkout works
|
|
67
|
+
in another path, another machine, another agent's worktree — nothing
|
|
68
|
+
about boot is anchored to an absolute location.
|
|
69
|
+
- Failure is immediate and names its cause: no `config/matsuri.rb` on
|
|
70
|
+
the walk up means no repo, before any command logic runs.
|
|
71
|
+
|
|
72
|
+
## Constraint: the repo contributes code through matsuri, not through its launcher
|
|
73
|
+
|
|
74
|
+
Immediately after discovery, matsuri prepends the repo's `lib/` to the
|
|
75
|
+
load path. The repo does not acquire a gemspec to get load-pathing from
|
|
76
|
+
bundler, and the wrapper does not export load paths. A platform repo is
|
|
77
|
+
an application; matsuri is what makes it loadable.
|
|
78
|
+
|
|
79
|
+
Properties:
|
|
80
|
+
|
|
81
|
+
- Requiring the repo's own library behaves identically under
|
|
82
|
+
`bundle exec`, a bare gem install, and a checkout. The repo's
|
|
83
|
+
extension pattern makes no assumption about who launched the process.
|
|
84
|
+
- The repo never misdeclares itself as a gem. No version, no files list,
|
|
85
|
+
no packaging promises made solely to obtain a load path.
|
|
86
|
+
- Every repo-authored file in the boot chain — configuration, platform
|
|
87
|
+
definitions, the initializer — may assume `lib/` is loadable, because
|
|
88
|
+
the load path precedes all of them.
|
|
89
|
+
|
|
90
|
+
## Constraint: the environment has exactly one source
|
|
91
|
+
|
|
92
|
+
The environment is the first positional argument:
|
|
93
|
+
`matsuri <env> <verb> ...`. There is no ambient override and no
|
|
94
|
+
detection from the binary's name. A repo that wants per-environment
|
|
95
|
+
entry points writes them as argument injection — `bin/staging` is
|
|
96
|
+
`exec bin/matsuri staging "$@"` — which keeps the sugar in the repo and
|
|
97
|
+
out of the grammar.
|
|
98
|
+
|
|
99
|
+
Properties:
|
|
100
|
+
|
|
101
|
+
- The command is self-describing. What was typed, what `ps` shows, what
|
|
102
|
+
the CI log recorded, and what matsuri resolved are the same string;
|
|
103
|
+
during an incident there is no exported variable to go find.
|
|
104
|
+
- One invocation grammar. Every surface that composes a matsuri command
|
|
105
|
+
— documentation, pipelines, agents, muscle memory — constructs the
|
|
106
|
+
same shape, and the environment slot cannot be consumed twice.
|
|
107
|
+
- Renaming or symlinking the binary is a non-event. No name is
|
|
108
|
+
significant, so no name is a trap.
|
|
109
|
+
|
|
110
|
+
## Constraint: declaration and activation are separate promises
|
|
111
|
+
|
|
112
|
+
The Gemfile declares what exists; the initializer activates what this
|
|
113
|
+
repo uses. Configuration (`config/matsuri.rb`, `config/platform.rb`) is
|
|
114
|
+
declarative — defaults and environment definitions, no requires. The
|
|
115
|
+
initializer (`config/initializer.rb`) is the one site that requires
|
|
116
|
+
plugins and the repo's own library, and it runs after configuration is
|
|
117
|
+
loaded, before any command logic.
|
|
118
|
+
|
|
119
|
+
Properties:
|
|
120
|
+
|
|
121
|
+
- A declared-but-unactivated plugin costs nothing and breaks nothing.
|
|
122
|
+
Adding a gem to the Gemfile is not yet a decision about boot.
|
|
123
|
+
- Activation order is repo-owned and readable in one file. What this
|
|
124
|
+
platform loads, and in what order, is the initializer — not a search
|
|
125
|
+
across configuration.
|
|
126
|
+
- Plugins activate against resolved configuration. By running after the
|
|
127
|
+
configuration layer, an initializer may branch on environment or
|
|
128
|
+
settings without ordering hazards.
|
|
129
|
+
- The boot sequence is a fixed spine — bound, discovered, loadable,
|
|
130
|
+
named, configured, activated, dispatched — and extension happens at
|
|
131
|
+
the one joint designed for it.
|
data/docs/design/ssa.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Server-Side Apply Design
|
|
2
|
+
|
|
3
|
+
This describes the constraints and properties of matsuri's server-side
|
|
4
|
+
delivery design: how one repo's declaration coexists with other writers
|
|
5
|
+
on the same Kubernetes objects. This is intended to provide both a
|
|
6
|
+
grammar for using server-side delivery and for extending it.
|
|
7
|
+
|
|
8
|
+
The promises mentioned in this document are understood as concepts from
|
|
9
|
+
Promise Theory.
|
|
10
|
+
|
|
11
|
+
## Framing
|
|
12
|
+
|
|
13
|
+
Under server-side apply, the apiserver merges by field ownership: an
|
|
14
|
+
object is a meeting place of managers, each owning the fields it last
|
|
15
|
+
sent. The merge is a fold across parties, but not a defined fold — the
|
|
16
|
+
managers are unordered, and there is no uniform identity element, so the
|
|
17
|
+
structure is not fully monoidal. Each party can promise only its own
|
|
18
|
+
fields.
|
|
19
|
+
|
|
20
|
+
Matsuri's position in that ensemble is fixed: own exactly what the
|
|
21
|
+
definition declares, cede everything else. Every constraint below exists
|
|
22
|
+
to hold that position. The seeding protocol exists because creation is
|
|
23
|
+
the one moment the position cannot be held by a single apply.
|
|
24
|
+
|
|
25
|
+
## Constraint: ownership follows declaration, and nothing is read back
|
|
26
|
+
|
|
27
|
+
The ongoing payload contains exactly the fields the repo manages.
|
|
28
|
+
Matsuri never reads live cluster state to decide a declaration: a field
|
|
29
|
+
another party rolls — an image a deployment operator delivers, a count
|
|
30
|
+
an autoscaler moves — is omitted from the payload, not read back and
|
|
31
|
+
re-sent.
|
|
32
|
+
|
|
33
|
+
Properties:
|
|
34
|
+
|
|
35
|
+
- The declaration is the ownership record. What the repo owns on the
|
|
36
|
+
cluster is what its definitions say, reviewable in the same diff that
|
|
37
|
+
reviews any other change; there is no second registry of claims.
|
|
38
|
+
- Other writers take their fields uncontested. A controller that scales
|
|
39
|
+
or a deliverer that rolls an image is never fought, because the repo
|
|
40
|
+
never speaks about those fields at all — there is no flapping between
|
|
41
|
+
two sources of truth.
|
|
42
|
+
- A conflict is information. Because the payload never carries a field
|
|
43
|
+
by accident of read-back, a conflict always marks a field two parties
|
|
44
|
+
genuinely both claim — never noise from a value that merely drifted.
|
|
45
|
+
|
|
46
|
+
## Constraint: empty is the identity element, and the identity is never serialized
|
|
47
|
+
|
|
48
|
+
An empty value in a payload is an ownership claim over nothing — and the
|
|
49
|
+
server does not persist it. Persistence normalizes empties away, and
|
|
50
|
+
inside an atomic struct the normalized persisted value and a serialized
|
|
51
|
+
empty diverge; under field ownership that divergence is a conflict, not
|
|
52
|
+
a merge. So payloads declare in persistence normal form: nil and empty
|
|
53
|
+
collections are pruned at the delivery boundary, and ceding a field and
|
|
54
|
+
saying nothing are structurally the same act.
|
|
55
|
+
|
|
56
|
+
Because the merge is not fully monoidal, the pruning carries an explicit
|
|
57
|
+
exception list: for some fields an empty value is a genuine assertion —
|
|
58
|
+
a field held empty against other writers — and those are declared by
|
|
59
|
+
path.
|
|
60
|
+
|
|
61
|
+
Properties:
|
|
62
|
+
|
|
63
|
+
- The create's echo equals the declaration. What the server persists
|
|
64
|
+
from the seeded create is value-equal to what the ongoing apply sends,
|
|
65
|
+
so the equal-values co-ownership the seeding protocol depends on holds
|
|
66
|
+
— including inside atomic structs, where any divergence is loud.
|
|
67
|
+
- Generation stays unconditional. A definition may render its empty
|
|
68
|
+
collections unconditionally, the natural shape for a generator; the
|
|
69
|
+
delivery boundary, not the generator, knows persistence normal form.
|
|
70
|
+
- Emptiness never claims a field. A rendered-but-empty collection cannot
|
|
71
|
+
quietly make the repo co-owner of something it does not manage.
|
|
72
|
+
- An empty value that reaches the cluster is deliberate. It survives
|
|
73
|
+
only by appearing in the exception list, so it is named, reviewable,
|
|
74
|
+
and distinguishable from an accident of rendering.
|
|
75
|
+
|
|
76
|
+
## Constraint: creation is a protocol, not a verb
|
|
77
|
+
|
|
78
|
+
A seeded field — the boot image, an initial replica count — must exist
|
|
79
|
+
from the seeded create and end owned by no one, because the ongoing
|
|
80
|
+
declaration never carries it. No single apply can promise both. Creation
|
|
81
|
+
therefore runs as three acts: create the seeded payload under the seed
|
|
82
|
+
owner (a manager named for the ongoing one, with a seed suffix), deliver
|
|
83
|
+
the declared payload with an ordinary server-side apply, then cede —
|
|
84
|
+
remove the seed owner's managedFields entry with an atomic
|
|
85
|
+
test-and-remove. The order is not stylistic: an apply against an object
|
|
86
|
+
with empty managedFields attributes every pre-existing field to a
|
|
87
|
+
synthetic manager that never applies again and so never relinquishes.
|
|
88
|
+
|
|
89
|
+
Properties:
|
|
90
|
+
|
|
91
|
+
- Seeds end unowned. The first writer to touch one — a controller
|
|
92
|
+
scaling, a deliverer rolling the image — takes it without conflict,
|
|
93
|
+
which is the entire point of seeding.
|
|
94
|
+
- The ongoing manager ends sole owner of exactly the declared fields,
|
|
95
|
+
and every later delivery is an ordinary apply. The protocol is paid
|
|
96
|
+
once, at seeding.
|
|
97
|
+
- An interrupted protocol stays attributable. The seed owner's name is
|
|
98
|
+
derived from the field manager, so a converge that dies mid-protocol
|
|
99
|
+
strands an object whose managedFields still read as this repo's —
|
|
100
|
+
failure does not orphan the claim.
|
|
101
|
+
- Re-creation is loud. Creating over an existing object fails rather
|
|
102
|
+
than silently re-seeding fields that other writers may have moved.
|
|
103
|
+
|
|
104
|
+
## Constraint: a conflict stops delivery, and forcing is a human judgment
|
|
105
|
+
|
|
106
|
+
Delivery never forces through a conflict. A conflict marks the frontier
|
|
107
|
+
where another manager owns a field this declaration sends; crossing it
|
|
108
|
+
transfers ownership and overwrites another party's promise. That is an
|
|
109
|
+
incident-time judgment, made by a person with the context in front of
|
|
110
|
+
them — never a delivery default. The escape hatch is raw kubectl, not a
|
|
111
|
+
flag.
|
|
112
|
+
|
|
113
|
+
Properties:
|
|
114
|
+
|
|
115
|
+
- Delivery cannot silently take fields. The ownership frontier moves
|
|
116
|
+
only by an explicit human act, so every transfer has an author.
|
|
117
|
+
- The preview shares the frontier. Diff for a server-side resource
|
|
118
|
+
dry-runs under the same field manager delivery uses, so the conflict
|
|
119
|
+
diff surfaces is the conflict apply will hit — not a different one
|
|
120
|
+
under a different name.
|
|
121
|
+
- Resolution leaves a record. Either the definition stops declaring the
|
|
122
|
+
field — a reviewed cede — or an operator takes it deliberately, once,
|
|
123
|
+
outside matsuri. Both are visible acts; neither is a quiet override.
|
|
124
|
+
|
|
125
|
+
## Constraint: delivery mode and payload content are orthogonal axes
|
|
126
|
+
|
|
127
|
+
How a payload travels is the resource's delivery policy; what the
|
|
128
|
+
payload says is its presenter pipeline, assembled by intent regime —
|
|
129
|
+
bringing into existence, or delivering declared state. Server-side apply
|
|
130
|
+
is a value of the first axis only. Moving a resource between client-side
|
|
131
|
+
and server-side delivery never changes which presenters run.
|
|
132
|
+
|
|
133
|
+
Properties:
|
|
134
|
+
|
|
135
|
+
- Migration is a policy change, not a re-derivation. A resource moves
|
|
136
|
+
between delivery modes as a one-line change, with its pipeline — and
|
|
137
|
+
the review that ratified it — intact.
|
|
138
|
+
- One stack can carry both modes. Members migrate individually, so
|
|
139
|
+
adopting server-side delivery is incremental, not a flag day.
|
|
140
|
+
- The regimes stay about intent. Creation seeds and normal form attach
|
|
141
|
+
to what the payload means, so they hold identically under either
|
|
142
|
+
transport.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Stacks Design
|
|
2
|
+
|
|
3
|
+
This describes the constraints and properties of the stack design. This
|
|
4
|
+
is intended to provide both a grammar on how stacks can be used, but
|
|
5
|
+
also extended.
|
|
6
|
+
|
|
7
|
+
## Framing
|
|
8
|
+
|
|
9
|
+
A stack is the span of a single management promise: a bundle of
|
|
10
|
+
Kubernetes resources that one operator converges as one act. The noun
|
|
11
|
+
answers "what do we manage together?" — it deliberately does not answer
|
|
12
|
+
"what talks to what?" or "what constitutes the whole system?". Each
|
|
13
|
+
constraint below keeps that promise narrow; the properties follow from
|
|
14
|
+
the narrowness.
|
|
15
|
+
|
|
16
|
+
## Constraint: membership is decided by co-management only
|
|
17
|
+
|
|
18
|
+
A resource belongs to a stack because its operator wants to converge it
|
|
19
|
+
together with the others — one owner, one lifecycle, one converge act.
|
|
20
|
+
Runtime topology, architectural wholeness, and system boundaries are not
|
|
21
|
+
membership criteria.
|
|
22
|
+
|
|
23
|
+
Properties:
|
|
24
|
+
|
|
25
|
+
- The management partition is orthogonal to the runtime connector graph.
|
|
26
|
+
Two workloads may communicate heavily and live in different stacks;
|
|
27
|
+
two may never speak and share one. Runtime coupling does not force
|
|
28
|
+
co-membership, and co-membership promises nothing about reachability.
|
|
29
|
+
- One noun serves every scale. A stack may be an entire platform or a
|
|
30
|
+
single service with its sidecars; because the noun claims only
|
|
31
|
+
co-management, it does not lie at either extreme, and there is no
|
|
32
|
+
ladder of bigger nouns to invent.
|
|
33
|
+
- Membership is revisable. Moving a workload between stacks reassigns a
|
|
34
|
+
management promise, nothing more. The cost is confined to the cut: an
|
|
35
|
+
in-stack neighbor is reached by local name, an out-of-stack neighbor
|
|
36
|
+
by an address carried in configuration. The change is small, explicit,
|
|
37
|
+
and visible in the definitions.
|
|
38
|
+
- Wholeness is not required, but filled references are. A stack need not
|
|
39
|
+
contain everything it depends on; every dependency it does not contain
|
|
40
|
+
must resolve to an address.
|
|
41
|
+
|
|
42
|
+
## Constraint: instance data arrives as an argument, not as ambient state
|
|
43
|
+
|
|
44
|
+
A stack-config binds at the stack definition, as an argument to the
|
|
45
|
+
converge act. It is never folded into the global configuration, which
|
|
46
|
+
remains the environment's ambient state.
|
|
47
|
+
|
|
48
|
+
Properties:
|
|
49
|
+
|
|
50
|
+
- Many instances of one stack can exist in one environment. Ambient
|
|
51
|
+
state is process-global and would quietly promise a single instance;
|
|
52
|
+
an argument makes the instance count a non-event.
|
|
53
|
+
- No collision surface. Stack-config keys never enter the framework's
|
|
54
|
+
configuration keyspace, so no key can shadow or be shadowed by
|
|
55
|
+
framework configuration, and no merge semantics are ever needed.
|
|
56
|
+
- Provenance survives. The configuration that was passed and the
|
|
57
|
+
manifests that were rendered remain separately readable; nothing is
|
|
58
|
+
destroyed by merging.
|
|
59
|
+
|
|
60
|
+
## Constraint: one stack-config per instance, taken whole
|
|
61
|
+
|
|
62
|
+
The stack-config is a single value. The stack deconstructs it through
|
|
63
|
+
its own definitions; members never receive private variants.
|
|
64
|
+
|
|
65
|
+
Properties:
|
|
66
|
+
|
|
67
|
+
- Members cannot disagree. Every resource in the instance derives from
|
|
68
|
+
the same binding, so consistency across the instance is structural
|
|
69
|
+
rather than disciplined.
|
|
70
|
+
- Deconstruction is legible. Which fields a stack consumes is read
|
|
71
|
+
directly from its definitions, not from a merge trace.
|
|
72
|
+
|
|
73
|
+
## Constraint: instance identity is the (stack, stack-config) pair
|
|
74
|
+
|
|
75
|
+
A stack name alone names a definition. An instance is the definition
|
|
76
|
+
bound to a stack-config, and every operation on an instance addresses
|
|
77
|
+
the pair.
|
|
78
|
+
|
|
79
|
+
Properties:
|
|
80
|
+
|
|
81
|
+
- Instances of the same stack coexist without qualification schemes —
|
|
82
|
+
the identity that distinguishes them is already in the address.
|
|
83
|
+
- No operation is ambiguous in an environment holding many instances;
|
|
84
|
+
status, converge, and delete all name the same pair.
|
|
85
|
+
|
|
86
|
+
## Constraint: parse the grammar, never own the vocabulary
|
|
87
|
+
|
|
88
|
+
Matsuri validates that a stack-config is well-formed. It makes no claim
|
|
89
|
+
to know every key: definitions lift the fields they consume and are
|
|
90
|
+
silent about the rest.
|
|
91
|
+
|
|
92
|
+
Properties:
|
|
93
|
+
|
|
94
|
+
- The stack-config can be shared. One document may serve several
|
|
95
|
+
consumers (deployment tooling, controllers, humans); a partial
|
|
96
|
+
consumer that rejected unknown keys would claim the whole vocabulary
|
|
97
|
+
and break the sharing.
|
|
98
|
+
- The promise is honest. Which keys are consumed is decided by the
|
|
99
|
+
definitions that read them, which is not enumerable in advance;
|
|
100
|
+
failing loudly on "unknown" keys would promise an authority matsuri
|
|
101
|
+
cannot have. Strict vocabulary enforcement belongs to whatever system
|
|
102
|
+
owns the vocabulary, at its own boundary.
|
|
103
|
+
- Vocabulary can grow without gem changes. The gem ships the mechanism —
|
|
104
|
+
the flag, the loading, the binding; what any key means is defined
|
|
105
|
+
entirely by the repository's own definitions.
|
|
106
|
+
|
|
107
|
+
## Constraint: environment and stack-config vary on different axes
|
|
108
|
+
|
|
109
|
+
An environment is a committed, closed set naming a tier — where things
|
|
110
|
+
run and under what posture. A stack-config is an open set naming an
|
|
111
|
+
instance — what this particular deployment of a stack is. Neither
|
|
112
|
+
substitutes for the other.
|
|
113
|
+
|
|
114
|
+
Properties:
|
|
115
|
+
|
|
116
|
+
- Dynamic environments without environment proliferation. The
|
|
117
|
+
lifecycle-driven case (a short-lived instance per feature branch) is
|
|
118
|
+
carried by instance data; the tier set stays small, static, and
|
|
119
|
+
reviewable.
|
|
120
|
+
- Tier concerns stay in code review; instance concerns stay in data.
|
|
121
|
+
Changing a cluster mapping is a change to committed definitions;
|
|
122
|
+
spinning up another instance is a new stack-config file.
|
|
123
|
+
|
|
124
|
+
## Constraint: cross-stack connections are references with defaults; a pin is the only override
|
|
125
|
+
|
|
126
|
+
Where a stack depends on something outside itself, the dependency is a
|
|
127
|
+
reference. References resolve to a default — the standing instances of a
|
|
128
|
+
shared tier such as staging — and a stack-config may pin any reference
|
|
129
|
+
to a different address. Pinning is the only override form.
|
|
130
|
+
|
|
131
|
+
Properties:
|
|
132
|
+
|
|
133
|
+
- An instance is a sparse override. Instantiating a stack for a feature
|
|
134
|
+
costs the stack itself plus the pins that differ; everything not
|
|
135
|
+
mentioned follows the defaults. Cost scales with the diff, not with
|
|
136
|
+
the size of the platform.
|
|
137
|
+
- Composition is by reference, never co-location. Two stacks owned by
|
|
138
|
+
different operators compose by one pointing at the other's address —
|
|
139
|
+
two configs, two lifecycles, one field between them.
|
|
140
|
+
- Coordination is recorded, not minted. When two owners agree to wire
|
|
141
|
+
their instances together, the pin records the outcome of an agreement
|
|
142
|
+
made between people; the mechanism imposes no registry, broker, or
|
|
143
|
+
naming authority of its own.
|
|
144
|
+
|
|
145
|
+
## Vocabulary note
|
|
146
|
+
|
|
147
|
+
Names make implied promises, and the flag grammar is part of the name. A
|
|
148
|
+
plural noun with a source suffix (`--values-from=`) promises a document
|
|
149
|
+
folded into a keyspace — overlay and merge semantics. The singular
|
|
150
|
+
`--stack-config=` promises an argument the render is for. Stack-config
|
|
151
|
+
is defined only in matsuri's own terms, as stated above; resemblance to
|
|
152
|
+
concepts in other tools is incidental and carries none of their
|
|
153
|
+
contracts.
|
data/docs/diff.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Diff
|
|
2
|
+
|
|
3
|
+
Matsuri carries two diffs because there are two different questions to
|
|
4
|
+
ask before a converge, and no single comparison answers both.
|
|
5
|
+
|
|
6
|
+
**The server-side diff** (the default) answers: *what will the cluster
|
|
7
|
+
actually do with this manifest?* The payload — the exact payload `apply`
|
|
8
|
+
would send — is dry-run through the apiserver, where defaulting and
|
|
9
|
+
admission controllers run, and kubectl prints live-vs-admitted. If an
|
|
10
|
+
admission controller injects a sidecar, a default, or a label, it shows
|
|
11
|
+
here.
|
|
12
|
+
|
|
13
|
+
**The hash diff** (`--hash-diff`) answers: *how has the live object
|
|
14
|
+
drifted from my declaration?* The comparison runs locally against the
|
|
15
|
+
rendered manifest, and everything the cluster owns — `status`, the
|
|
16
|
+
last-applied annotation, server-populated defaults, equivalent
|
|
17
|
+
quantities like `1024Mi` vs `1Gi` — is filtered out, so what remains is
|
|
18
|
+
your drift, not the cluster's bookkeeping.
|
|
19
|
+
|
|
20
|
+
Use the default to preview a converge; use `--hash-diff` when the
|
|
21
|
+
admitted view drowns your one-line change in injected fields.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```console
|
|
26
|
+
$ matsuri staging diff deployment web # server-side: live vs admitted
|
|
27
|
+
$ matsuri staging diff deployment web --hash-diff # client-side: drift vs declaration
|
|
28
|
+
$ matsuri staging diff stack my-stack # every member, same flags
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Workload resource verbs accept an image tag positional, same as apply:
|
|
32
|
+
|
|
33
|
+
```console
|
|
34
|
+
$ matsuri staging diff deployment web v1.2.3
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Primary container focus
|
|
38
|
+
|
|
39
|
+
`-p` / `--primary-container` narrows the comparison to the first
|
|
40
|
+
container of the pod template — the usual question being "did anything
|
|
41
|
+
but my image change?":
|
|
42
|
+
|
|
43
|
+
```console
|
|
44
|
+
$ matsuri staging diff deployment web v1.2.3 -p
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The projection is a hash-diff affordance (kubectl has no equivalent), so
|
|
48
|
+
`-p` implies `--hash-diff`.
|
|
49
|
+
|
|
50
|
+
Every built-in workload with a pod template declares where its primary
|
|
51
|
+
container lives via `let(:primary_container_path)`; a custom resource
|
|
52
|
+
that wants `-p` declares the path into its own manifest shape:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
let(:primary_container_path) { ['spec', 'template', 'spec', 'containers', 0] }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Exit codes
|
|
59
|
+
|
|
60
|
+
The server-side diff inherits kubectl's convention:
|
|
61
|
+
|
|
62
|
+
- `0` — no differences (and no output; silence means clean)
|
|
63
|
+
- `1` — differences found (not an error)
|
|
64
|
+
- above `1` — an actual failure
|
|
65
|
+
|
|
66
|
+
## Tuning the hash diff filters
|
|
67
|
+
|
|
68
|
+
The noise filters live on the resource (`Matsuri::Concerns::Diffable`),
|
|
69
|
+
so a definition that knows about additional cluster-owned paths can
|
|
70
|
+
override them:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
Matsuri.define :deployment, 'web' do
|
|
74
|
+
# a controller rewrites this annotation on every sync; never report it
|
|
75
|
+
def always_changed_path?(path)
|
|
76
|
+
super || path == 'metadata.annotations.example.com/sync-stamp'
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The filter surface: `readonly_path?` (paths the apiserver owns),
|
|
82
|
+
`always_changed_path?` (values rewritten on every apply),
|
|
83
|
+
`default_value_assigned_nothing?` (declaring a default the server
|
|
84
|
+
already holds), and `unit_comparison_is_equal?` (quantity-aware
|
|
85
|
+
equality). Each returning `true` suppresses that comparison.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Known Issues
|
|
2
|
+
|
|
3
|
+
## CronJob templates inherit Pod's restartPolicy default
|
|
4
|
+
|
|
5
|
+
Pod defaults `restart_policy` to `Always`, and nothing in the CronJob
|
|
6
|
+
chain overrides it. Kubernetes rejects `Always` in Job pod templates
|
|
7
|
+
(`OnFailure` or `Never` are the lawful values), so a cron job built
|
|
8
|
+
through the pod template emits a template the API server refuses unless
|
|
9
|
+
the pod definition declares its own policy:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
Matsuri.define :pod, 'report' do
|
|
13
|
+
let(:restart_policy) { 'OnFailure' }
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The fixture pod `report` models this. Whether CronJob should own a
|
|
18
|
+
Job-lawful default for its template, rather than inheriting Pod's, is an
|
|
19
|
+
open design question — deferred.
|