matsuri 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. checksums.yaml +5 -5
  2. data/.envrc +1 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +9 -0
  5. data/AGENTS.md +18 -0
  6. data/CHANGELOG.md +642 -0
  7. data/Gemfile +2 -0
  8. data/README.md +86 -8
  9. data/Rakefile +20 -0
  10. data/bin/console +1 -0
  11. data/bin/matsuri +6 -0
  12. data/bin/setup +2 -0
  13. data/docs/acceptors.md +181 -0
  14. data/docs/boot.md +101 -0
  15. data/docs/design/acceptors.md +157 -0
  16. data/docs/design/boot.md +131 -0
  17. data/docs/design/ssa.md +142 -0
  18. data/docs/design/stacks.md +153 -0
  19. data/docs/diff.md +85 -0
  20. data/docs/known-issues.md +19 -0
  21. data/docs/local-dev.md +173 -0
  22. data/docs/presenters.md +317 -0
  23. data/docs/resources.md +121 -0
  24. data/docs/setup.md +97 -0
  25. data/docs/ssa.md +149 -0
  26. data/docs/stacks.md +146 -0
  27. data/flake.lock +61 -0
  28. data/flake.nix +35 -0
  29. data/lib/matsuri/acceptor.rb +33 -0
  30. data/lib/matsuri/acceptors/image_tag.rb +15 -0
  31. data/lib/matsuri/cmd.rb +46 -0
  32. data/lib/matsuri/cmds/apply.rb +78 -0
  33. data/lib/matsuri/cmds/cli.rb +147 -0
  34. data/lib/matsuri/cmds/create.rb +87 -0
  35. data/lib/matsuri/cmds/delete.rb +86 -0
  36. data/lib/matsuri/cmds/diff.rb +105 -0
  37. data/lib/matsuri/cmds/generate.rb +24 -0
  38. data/lib/matsuri/cmds/kubectl.rb +16 -0
  39. data/lib/matsuri/cmds/recreate.rb +65 -0
  40. data/lib/matsuri/cmds/scale.rb +37 -0
  41. data/lib/matsuri/cmds/show.rb +123 -0
  42. data/lib/matsuri/cmds/status.rb +67 -0
  43. data/lib/matsuri/concerns/awaiting.rb +24 -0
  44. data/lib/matsuri/concerns/diffable.rb +135 -0
  45. data/lib/matsuri/concerns/export_manifest.rb +56 -0
  46. data/lib/matsuri/concerns/import_manifest.rb +64 -0
  47. data/lib/matsuri/concerns/pod_template.rb +37 -0
  48. data/lib/matsuri/concerns/pods/spread_pods.rb +47 -0
  49. data/lib/matsuri/concerns/pods/spread_topology.rb +63 -0
  50. data/lib/matsuri/concerns/registry_helpers.rb +59 -0
  51. data/lib/matsuri/config.rb +101 -0
  52. data/lib/matsuri/core_ext.rb +107 -0
  53. data/lib/matsuri/dsl/cluster/aggregated_cluster_role.rb +75 -0
  54. data/lib/matsuri/dsl/cluster/binding.rb +118 -0
  55. data/lib/matsuri/dsl/cluster/cluster_role.rb +41 -0
  56. data/lib/matsuri/dsl/cluster/role.rb +49 -0
  57. data/lib/matsuri/dsl/cluster/scope.rb +245 -0
  58. data/lib/matsuri/dsl/cluster/service_account.rb +76 -0
  59. data/lib/matsuri/dsl/concerns/default_binding.rb +34 -0
  60. data/lib/matsuri/dsl/concerns/manifest_set.rb +21 -0
  61. data/lib/matsuri/dsl/concerns/metadata.rb +56 -0
  62. data/lib/matsuri/dsl/concerns/rbac_rules.rb +39 -0
  63. data/lib/matsuri/initializer.rb +41 -0
  64. data/lib/matsuri/kubernetes/base.rb +107 -0
  65. data/lib/matsuri/kubernetes/config_map.rb +100 -0
  66. data/lib/matsuri/kubernetes/cron_job.rb +115 -0
  67. data/lib/matsuri/kubernetes/daemon_set.rb +62 -0
  68. data/lib/matsuri/kubernetes/deployment.rb +65 -0
  69. data/lib/matsuri/kubernetes/endpoints.rb +57 -0
  70. data/lib/matsuri/kubernetes/horizontal_pod_autoscaler.rb +116 -0
  71. data/lib/matsuri/kubernetes/imported_manifest.rb +61 -0
  72. data/lib/matsuri/kubernetes/ingress.rb +66 -0
  73. data/lib/matsuri/kubernetes/persistent_volume.rb +56 -0
  74. data/lib/matsuri/kubernetes/persistent_volume_claim.rb +41 -0
  75. data/lib/matsuri/kubernetes/pod.rb +355 -0
  76. data/lib/matsuri/kubernetes/replica_set.rb +46 -0
  77. data/lib/matsuri/kubernetes/secret.rb +86 -0
  78. data/lib/matsuri/kubernetes/service.rb +49 -0
  79. data/lib/matsuri/kubernetes/stateful_set.rb +75 -0
  80. data/lib/matsuri/kubernetes/storage_class.rb +45 -0
  81. data/lib/matsuri/platform.rb +38 -0
  82. data/lib/matsuri/presenter.rb +47 -0
  83. data/lib/matsuri/presenters/helpers.rb +39 -0
  84. data/lib/matsuri/presenters/prune_empty.rb +59 -0
  85. data/lib/matsuri/presenters/seed_image.rb +28 -0
  86. data/lib/matsuri/presenters/seed_replicas.rb +25 -0
  87. data/lib/matsuri/presenters/seed_resources.rb +29 -0
  88. data/lib/matsuri/registry.rb +267 -0
  89. data/lib/matsuri/shell_out.rb +119 -0
  90. data/lib/matsuri/stack.rb +276 -0
  91. data/lib/matsuri/task.rb +20 -0
  92. data/lib/matsuri/tasks/cluster.rb +79 -0
  93. data/lib/matsuri/tasks/docker.rb +25 -0
  94. data/lib/matsuri/tasks/hash_diff.rb +61 -0
  95. data/lib/matsuri/tasks/kubectl.rb +21 -0
  96. data/lib/matsuri/tasks/kubectl_ops/apply.rb +24 -0
  97. data/lib/matsuri/tasks/kubectl_ops/create.rb +27 -0
  98. data/lib/matsuri/tasks/kubectl_ops/delete.rb +21 -0
  99. data/lib/matsuri/tasks/kubectl_ops/diff.rb +47 -0
  100. data/lib/matsuri/tasks/kubectl_ops/get.rb +36 -0
  101. data/lib/matsuri/tasks/kubectl_ops/recreate.rb +35 -0
  102. data/lib/matsuri/tasks/kubectl_ops/replace.rb +29 -0
  103. data/lib/matsuri/tasks/kubectl_ops/rollout_status.rb +22 -0
  104. data/lib/matsuri/tasks/kubectl_ops/scale.rb +22 -0
  105. data/lib/matsuri/tasks/kubectl_ops/server_side_apply.rb +32 -0
  106. data/lib/matsuri/tasks/kubectl_ops/status.rb +20 -0
  107. data/lib/matsuri/tasks/kubectl_ops.rb +218 -0
  108. data/lib/matsuri/tasks/pod.rb +56 -0
  109. data/lib/matsuri/tasks/stacks/seed.rb +93 -0
  110. data/lib/matsuri/tasks/stacks.rb +31 -0
  111. data/lib/matsuri/transform/algebra.rb +52 -0
  112. data/lib/matsuri/transform/callable.rb +54 -0
  113. data/lib/matsuri/transform/combinators.rb +61 -0
  114. data/lib/matsuri/transform/env.rb +43 -0
  115. data/lib/matsuri/transform/kubernetes.rb +89 -0
  116. data/lib/matsuri/transform.rb +20 -0
  117. data/lib/matsuri/version.rb +3 -1
  118. data/lib/matsuri.rb +158 -2
  119. data/matsuri.gemspec +20 -7
  120. data/tests/bin/minikube-start.sh +27 -0
  121. data/tests/bin/probe-save-config.sh +46 -0
  122. data/tests/bin/probe-ssa.sh +206 -0
  123. data/tests/example/.gitignore +1 -0
  124. data/tests/example/Gemfile +7 -0
  125. data/tests/example/README.md +91 -0
  126. data/tests/example/bin/matsuri +14 -0
  127. data/tests/example/config/initializer.rb +12 -0
  128. data/tests/example/config/matsuri.rb +9 -0
  129. data/tests/example/config/platform.rb +39 -0
  130. data/tests/example/lib/acme/nodes/dedicated.rb +26 -0
  131. data/tests/example/lib/acme/nodes/preemptible.rb +17 -0
  132. data/tests/example/lib/acme/nodes/required_cores.rb +26 -0
  133. data/tests/example/lib/acme/nodes/resource_class.rb +26 -0
  134. data/tests/example/lib/acme/pods/cacerts.rb +15 -0
  135. data/tests/example/lib/acme/pods/healthcheck.rb +36 -0
  136. data/tests/example/lib/acme.rb +26 -0
  137. data/tests/example/platform/config_maps/billing-settings.rb +8 -0
  138. data/tests/example/platform/config_maps/cacerts.rb +9 -0
  139. data/tests/example/platform/deployments/billing-web.rb +5 -0
  140. data/tests/example/platform/deployments/checkout-web.rb +6 -0
  141. data/tests/example/platform/namespaces/billing-dev.yaml +7 -0
  142. data/tests/example/platform/namespaces/checkout-dev.yaml +7 -0
  143. data/tests/example/platform/pods/billing.rb +31 -0
  144. data/tests/example/platform/pods/checkout.rb +27 -0
  145. data/tests/example/platform/services/billing-web.rb +6 -0
  146. data/tests/example/platform/services/checkout-web.rb +4 -0
  147. data/tests/example/platform/stacks/billing.rb +8 -0
  148. data/tests/example/platform/stacks/checkout.rb +19 -0
  149. data/tests/k8s/save-config/deployment-declared.yaml +19 -0
  150. data/tests/k8s/save-config/deployment-seeded.yaml +20 -0
  151. data/tests/k8s/save-config/namespace.yaml +4 -0
  152. data/tests/k8s/ssa/declared-bumped.yaml +22 -0
  153. data/tests/k8s/ssa/declared.yaml +22 -0
  154. data/tests/k8s/ssa/namespace.yaml +4 -0
  155. data/tests/k8s/ssa/seed-via-create.yaml +24 -0
  156. metadata +288 -20
  157. data/.travis.yml +0 -4
@@ -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.
@@ -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.