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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Probes the creation protocol the seeds+SSA posture rests on, before any
|
|
5
|
+
# SSA code lands. Hard expectations (FAIL) pin the invariants the design
|
|
6
|
+
# assumes; REPORT legs are open questions whose answers shape the design.
|
|
7
|
+
#
|
|
8
|
+
# The seed that matters is the IMAGE: the ongoing declaration omits it
|
|
9
|
+
# (the container entry rides on its name key alone) and another writer
|
|
10
|
+
# delivers it. Replicas is the second seed; a declared env var is the
|
|
11
|
+
# ongoing manager's own changeable field.
|
|
12
|
+
#
|
|
13
|
+
# F. seed via create under the seed owner (a disposable manager derived
|
|
14
|
+
# from the ongoing one), then the ordinary declared SSA apply, then
|
|
15
|
+
# remove the seed owner's managedFields entry with an atomic
|
|
16
|
+
# test+remove JSON patch.
|
|
17
|
+
# managedFields is never empty when an apply arrives (so the server
|
|
18
|
+
# never invents before-first-apply), the seeds end unowned, and the
|
|
19
|
+
# ongoing applier ends sole owner of the declared fields.
|
|
20
|
+
# F1 EXPECT create --field-manager=<seed owner> lands both seeds
|
|
21
|
+
# F2 EXPECT the declared apply is accepted with the image absent
|
|
22
|
+
# from the container entry (keyed-list partial), seeds
|
|
23
|
+
# untouched, and the ongoing manager never owns image
|
|
24
|
+
# F3 EXPECT test+remove of the seed owner's entry applies (<=3
|
|
25
|
+
# attempts; the test op makes a stale index a loud
|
|
26
|
+
# retry, not a wrong removal)
|
|
27
|
+
# F4 EXPECT values intact; seed owner entry gone; image AND replicas
|
|
28
|
+
# unowned; raw managedFields dumped for verification
|
|
29
|
+
# F5 EXPECT changing a declared value is clean -- the leg every
|
|
30
|
+
# refuted path failed -- and both seeds are untouched
|
|
31
|
+
# F6 EXPECT image delivery via kubectl set image (update-family)
|
|
32
|
+
# is clean, and the next omitting apply leaves the
|
|
33
|
+
# delivered image alone
|
|
34
|
+
# F7 EXPECT after kubectl scale, an omitting apply leaves
|
|
35
|
+
# replicas alone
|
|
36
|
+
# F8 EXPECT a different manager changing an owned field conflicts
|
|
37
|
+
# F9 REPORT diff --server-side surfaces that conflict (exit 2)
|
|
38
|
+
# F10 EXPECT re-running the seeded create is loud (AlreadyExists)
|
|
39
|
+
# F11 REPORT last-applied state; EXPECT it never carries a seed
|
|
40
|
+
# (the CSA prune trap)
|
|
41
|
+
#
|
|
42
|
+
# Ownership claims are live reads: owners() runs a fresh get
|
|
43
|
+
# --show-managed-fields per call and scans every entry's fieldsV1 for
|
|
44
|
+
# the field's path; the raw dumps in F4/F6/F7 print the entries
|
|
45
|
+
# verbatim (status/metadata pruned) so "unowned" is checkable against
|
|
46
|
+
# the record, not the filter.
|
|
47
|
+
#
|
|
48
|
+
# Refuted alternatives, pruned from the probe after earlier runs:
|
|
49
|
+
# - create then SSA directly: the create manager's Update entry co-owns every
|
|
50
|
+
# seeded field and never relinquishes; the first changed-value apply
|
|
51
|
+
# conflicts permanently.
|
|
52
|
+
# - create under the SAME name the applies use: an Update entry and an
|
|
53
|
+
# Apply entry are distinct managers; the conflict remains.
|
|
54
|
+
# - seed on the first SSA apply: the next apply that omits the seed
|
|
55
|
+
# relinquishes the field and the server resets it.
|
|
56
|
+
# - wipe managedFields BEFORE the first apply (patch or embedded in the
|
|
57
|
+
# create body): an apply against empty managedFields attributes every
|
|
58
|
+
# existing field to the synthetic manager before-first-apply, which
|
|
59
|
+
# never relinquishes; the same dead end under another name.
|
|
60
|
+
#
|
|
61
|
+
# Start the cluster with bin/minikube-start.sh first. kubectl dials the
|
|
62
|
+
# VM: on macOS run from a Local Network-granted context (not tmux).
|
|
63
|
+
|
|
64
|
+
TESTS_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
65
|
+
CONTEXT="${CONTEXT:-matsuri}"
|
|
66
|
+
MANAGER="${MANAGER:-matsuri}"
|
|
67
|
+
OTHER_MANAGER="${OTHER_MANAGER:-other}"
|
|
68
|
+
# Disposable but deliberate: the F3 removal predicate keys on this name,
|
|
69
|
+
# and a converge that dies before F3 strands an object whose managedFields
|
|
70
|
+
# should read as ours. The seed owner is derived from the ongoing manager
|
|
71
|
+
# (<manager>-seed), matching Tasks::Stacks::Seed.
|
|
72
|
+
CREATE_MANAGER="${CREATE_MANAGER:-${MANAGER}-seed}"
|
|
73
|
+
K8S="${TESTS_DIR}/k8s/ssa"
|
|
74
|
+
KUBECTL=(kubectl --context "${CONTEXT}" --namespace matsuri-probes)
|
|
75
|
+
|
|
76
|
+
fail() { echo "FAIL: $*" >&2; exit 1; }
|
|
77
|
+
report() { echo "REPORT $*"; }
|
|
78
|
+
|
|
79
|
+
ssa() { "${KUBECTL[@]}" apply --server-side --field-manager="${MANAGER}" -f "$1"; }
|
|
80
|
+
|
|
81
|
+
replicas() { "${KUBECTL[@]}" get deployment "$1" -o jsonpath='{.spec.replicas}'; }
|
|
82
|
+
image() { "${KUBECTL[@]}" get deployment "$1" -o jsonpath='{.spec.template.spec.containers[0].image}'; }
|
|
83
|
+
declared() { "${KUBECTL[@]}" get deployment "$1" -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="DECLARED")].value}'; }
|
|
84
|
+
|
|
85
|
+
# every manager/operation pair on the object
|
|
86
|
+
managers() { # managers <deployment>
|
|
87
|
+
"${KUBECTL[@]}" get deployment "$1" --show-managed-fields -o json |
|
|
88
|
+
jq -r '[.metadata.managedFields[]? | "\(.manager)/\(.operation)"] | join(" ")'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# manager/operation pairs owning the given field, from a fresh read
|
|
92
|
+
owners() { # owners <deployment> replicas|image|env
|
|
93
|
+
local path
|
|
94
|
+
case "$2" in
|
|
95
|
+
replicas) path='["fieldsV1","f:spec","f:replicas"]' ;;
|
|
96
|
+
image) path='["fieldsV1","f:spec","f:template","f:spec","f:containers","k:{\"name\":\"pause\"}","f:image"]' ;;
|
|
97
|
+
env) path='["fieldsV1","f:spec","f:template","f:spec","f:containers","k:{\"name\":\"pause\"}","f:env","k:{\"name\":\"DECLARED\"}"]' ;;
|
|
98
|
+
esac
|
|
99
|
+
"${KUBECTL[@]}" get deployment "$1" --show-managed-fields -o json |
|
|
100
|
+
jq -r --argjson p "${path}" '.metadata.managedFields[] | select(getpath($p) != null) | "\(.manager)/\(.operation)"' |
|
|
101
|
+
paste -sd ' ' -
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# the managedFields entries verbatim, status/metadata pruned for reading
|
|
105
|
+
managed_fields_raw() { # managed_fields_raw <deployment>
|
|
106
|
+
"${KUBECTL[@]}" get deployment "$1" --show-managed-fields -o json |
|
|
107
|
+
jq -c '.metadata.managedFields[]? | {manager, operation, subresource, fieldsV1: (.fieldsV1 | del(."f:status", ."f:metadata"))}' |
|
|
108
|
+
sed 's/^/ /'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
report "versions: $(kubectl --context "${CONTEXT}" version -o json 2>/dev/null | jq -r '"client \(.clientVersion.gitVersion) / server \(.serverVersion.gitVersion)"')"
|
|
112
|
+
|
|
113
|
+
kubectl --context "${CONTEXT}" apply -f "${K8S}/namespace.yaml"
|
|
114
|
+
"${KUBECTL[@]}" delete deployment seed-via-create --ignore-not-found --wait
|
|
115
|
+
|
|
116
|
+
echo '--- Path F: seed via create, declared apply, then remove the seed owner entry ---'
|
|
117
|
+
|
|
118
|
+
"${KUBECTL[@]}" create --field-manager="${CREATE_MANAGER}" -f "${K8S}/seed-via-create.yaml"
|
|
119
|
+
[ "$(replicas seed-via-create)" = "3" ] || fail "F1 create did not land the seeded replicas (replicas=$(replicas seed-via-create))"
|
|
120
|
+
[ "$(image seed-via-create)" = "registry.k8s.io/pause:3.9" ] || fail "F1 create did not land the seeded image (image=$(image seed-via-create))"
|
|
121
|
+
report "F1 managers at birth: [$(managers seed-via-create)]"
|
|
122
|
+
|
|
123
|
+
ssa "${K8S}/declared.yaml" || fail "F2 the declared apply was rejected -- a container entry cannot omit its image"
|
|
124
|
+
[ "$(replicas seed-via-create)" = "3" ] || fail "F2 declared apply disturbed the seeded replicas (replicas=$(replicas seed-via-create))"
|
|
125
|
+
[ "$(image seed-via-create)" = "registry.k8s.io/pause:3.9" ] || fail "F2 declared apply disturbed the seeded image (image=$(image seed-via-create))"
|
|
126
|
+
case " $(owners seed-via-create image) " in
|
|
127
|
+
*" ${MANAGER}/"*) fail "F2 the ongoing manager owns the image it never sent" ;;
|
|
128
|
+
esac
|
|
129
|
+
report "F2 after declared apply: image owned by [$(owners seed-via-create image)], env owned by [$(owners seed-via-create env)]"
|
|
130
|
+
|
|
131
|
+
patched=""
|
|
132
|
+
for attempt in 1 2 3; do
|
|
133
|
+
idx="$("${KUBECTL[@]}" get deployment seed-via-create --show-managed-fields -o json |
|
|
134
|
+
jq --arg m "${CREATE_MANAGER}" '[.metadata.managedFields[].manager] | index($m)')"
|
|
135
|
+
[ "${idx}" != "null" ] || fail "F3 no ${CREATE_MANAGER} entry to remove"
|
|
136
|
+
if "${KUBECTL[@]}" patch deployment seed-via-create --type=json -p "[
|
|
137
|
+
{\"op\":\"test\",\"path\":\"/metadata/managedFields/${idx}/manager\",\"value\":\"${CREATE_MANAGER}\"},
|
|
138
|
+
{\"op\":\"remove\",\"path\":\"/metadata/managedFields/${idx}\"}
|
|
139
|
+
]"; then
|
|
140
|
+
patched="yes"
|
|
141
|
+
[ "${attempt}" = "1" ] || report "F3 removal needed ${attempt} attempts (object changed under the read)"
|
|
142
|
+
break
|
|
143
|
+
fi
|
|
144
|
+
done
|
|
145
|
+
[ -n "${patched}" ] || fail "F3 test+remove lost the race 3 times"
|
|
146
|
+
report "F3 managers after removal: [$(managers seed-via-create)]"
|
|
147
|
+
|
|
148
|
+
[ "$(replicas seed-via-create)" = "3" ] || fail "F4 the removal disturbed the seeded replicas (replicas=$(replicas seed-via-create))"
|
|
149
|
+
[ "$(image seed-via-create)" = "registry.k8s.io/pause:3.9" ] || fail "F4 the removal disturbed the seeded image (image=$(image seed-via-create))"
|
|
150
|
+
case " $(managers seed-via-create) " in
|
|
151
|
+
*" ${CREATE_MANAGER}/"*) fail "F4 the ${CREATE_MANAGER} entry survived the removal" ;;
|
|
152
|
+
esac
|
|
153
|
+
[ -z "$(owners seed-via-create image)" ] || fail "F4 image still owned by [$(owners seed-via-create image)] -- expected unowned"
|
|
154
|
+
[ -z "$(owners seed-via-create replicas)" ] || fail "F4 replicas still owned by [$(owners seed-via-create replicas)] -- expected unowned"
|
|
155
|
+
report "F4 seeds unowned, env owned by [$(owners seed-via-create env)]; managedFields verbatim:"
|
|
156
|
+
managed_fields_raw seed-via-create
|
|
157
|
+
|
|
158
|
+
ssa "${K8S}/declared-bumped.yaml" || fail "F5 changing a declared value conflicted -- the dead-end survives the seed-owner removal"
|
|
159
|
+
[ "$(declared seed-via-create)" = "two" ] || fail "F5 the declared change did not land (DECLARED=$(declared seed-via-create))"
|
|
160
|
+
[ "$(replicas seed-via-create)" = "3" ] || fail "F5 the declared change disturbed the unowned replicas (replicas=$(replicas seed-via-create)) -- a ghost manager came back"
|
|
161
|
+
[ "$(image seed-via-create)" = "registry.k8s.io/pause:3.9" ] || fail "F5 the declared change disturbed the unowned image (image=$(image seed-via-create)) -- a ghost manager came back"
|
|
162
|
+
echo "F5 changed-declared apply clean -- the ongoing-delivery dead-end is gone"
|
|
163
|
+
report "F5 env owned by [$(owners seed-via-create env)]"
|
|
164
|
+
|
|
165
|
+
"${KUBECTL[@]}" set image deployment/seed-via-create pause=registry.k8s.io/pause:3.10
|
|
166
|
+
[ "$(image seed-via-create)" = "registry.k8s.io/pause:3.10" ] || fail "F6 image delivery did not land (image=$(image seed-via-create))"
|
|
167
|
+
ssa "${K8S}/declared-bumped.yaml"
|
|
168
|
+
[ "$(image seed-via-create)" = "registry.k8s.io/pause:3.10" ] || fail "F6 omitting apply disturbed the delivered image (image=$(image seed-via-create))"
|
|
169
|
+
report "F6 delivered image stands; image owned by [$(owners seed-via-create image)]; managedFields verbatim:"
|
|
170
|
+
managed_fields_raw seed-via-create
|
|
171
|
+
|
|
172
|
+
"${KUBECTL[@]}" scale deployment seed-via-create --replicas=5
|
|
173
|
+
ssa "${K8S}/declared-bumped.yaml"
|
|
174
|
+
[ "$(replicas seed-via-create)" = "5" ] || fail "F7 omitting apply disturbed scaled replicas (replicas=$(replicas seed-via-create))"
|
|
175
|
+
report "F7 after scale: replicas owned by [$(owners seed-via-create replicas)]; managedFields verbatim:"
|
|
176
|
+
managed_fields_raw seed-via-create
|
|
177
|
+
|
|
178
|
+
if out="$("${KUBECTL[@]}" apply --server-side --field-manager="${OTHER_MANAGER}" -f "${K8S}/declared.yaml" 2>&1)"; then
|
|
179
|
+
fail "F8 a different manager changed an owned field without conflict"
|
|
180
|
+
fi
|
|
181
|
+
echo "F8 cross-manager change conflicts (frontier intact)"
|
|
182
|
+
|
|
183
|
+
set +e
|
|
184
|
+
out="$("${KUBECTL[@]}" diff --server-side --field-manager="${OTHER_MANAGER}" -f "${K8S}/declared.yaml" 2>&1)"
|
|
185
|
+
rc=$?
|
|
186
|
+
set -e
|
|
187
|
+
report "F9 diff --server-side of the conflicting payload: exit=${rc} (2 = conflict surfaced)"
|
|
188
|
+
echo "${out}" | head -5 | sed 's/^/ /'
|
|
189
|
+
|
|
190
|
+
if out="$("${KUBECTL[@]}" create --field-manager="${CREATE_MANAGER}" -f "${K8S}/seed-via-create.yaml" 2>&1)"; then
|
|
191
|
+
fail "F10 re-running the seeded create silently succeeded"
|
|
192
|
+
fi
|
|
193
|
+
echo "F10 re-running the seeded create is loud:"
|
|
194
|
+
echo "${out}" | sed 's/^/ /'
|
|
195
|
+
|
|
196
|
+
if la="$("${KUBECTL[@]}" apply view-last-applied deployment seed-via-create 2>/dev/null)"; then
|
|
197
|
+
report "F11 last-applied content:"
|
|
198
|
+
echo "${la}" | sed 's/^/ /'
|
|
199
|
+
if echo "${la}" | grep -Eq 'replicas|image'; then
|
|
200
|
+
fail "F11 last-applied carries a seed -- the CSA prune trap is loaded"
|
|
201
|
+
fi
|
|
202
|
+
else
|
|
203
|
+
report "F11 no last-applied annotation"
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
echo "PASS: the creation protocol held; read the REPORT lines for ownership detail"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Gemfile.lock
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Example Matsuri Repo
|
|
2
|
+
|
|
3
|
+
A working Matsuri platform repo. It boots against the minikube test
|
|
4
|
+
cluster, and it showcases the layout a real repo follows (see
|
|
5
|
+
docs/setup.md in the matsuri repo): a Gemfile pinning the matsuri
|
|
6
|
+
version, a `bin/matsuri` wrapper, config, an org extension library
|
|
7
|
+
(`lib/acme`), and resource definitions under `platform/`.
|
|
8
|
+
|
|
9
|
+
Two stacks demonstrate the two delivery modes:
|
|
10
|
+
|
|
11
|
+
- **billing** — classic client-side delivery. Matsuri owns every
|
|
12
|
+
field: replicas are declared, the image tracks the converge
|
|
13
|
+
positional.
|
|
14
|
+
- **checkout** — server-side delivery. The deployment applies by field
|
|
15
|
+
ownership; matsuri omits the image, replicas, and resources, seeds
|
|
16
|
+
them at creation, and cedes them to the operators that will own them
|
|
17
|
+
(deployment operator, HPA, in-place autoscaler).
|
|
18
|
+
|
|
19
|
+
## Prerequisites
|
|
20
|
+
|
|
21
|
+
The minikube test cluster must be running under the `matsuri` profile:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# from the matsuri repo root
|
|
25
|
+
tests/bin/minikube-start.sh
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The script also applies `platform/namespaces/` — raw manifests, outside
|
|
29
|
+
matsuri, because namespaces precede anything matsuri converges.
|
|
30
|
+
|
|
31
|
+
**macOS gotcha:** kubectl and minikube dial the VM over the local
|
|
32
|
+
network, which macOS gates behind a Local Network permission. Run from
|
|
33
|
+
a terminal that has the grant (System Settings → Privacy & Security →
|
|
34
|
+
Local Network). tmux counts as its own responsible process and cannot
|
|
35
|
+
be granted from the CLI — `no route to host` from inside tmux while the
|
|
36
|
+
cluster is fine is this, not the cluster.
|
|
37
|
+
|
|
38
|
+
Then, from this directory:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bundle install
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Creating the Stacks
|
|
45
|
+
|
|
46
|
+
The first converge creates everything a stack needs:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bin/matsuri local_billing_dev converge billing
|
|
50
|
+
bin/matsuri local_checkout_dev converge checkout
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Each stack has its own local environment, pointed at the minikube
|
|
54
|
+
cluster with its own namespace (`billing-dev`, `checkout-dev`); see
|
|
55
|
+
`config/platform.rb`. Checkout's first converge routes through the
|
|
56
|
+
seeding protocol: a seeded create completes the boot payload, the
|
|
57
|
+
declared server-side apply takes ownership of matsuri's fields, and the
|
|
58
|
+
seed owner cedes the rest.
|
|
59
|
+
|
|
60
|
+
## Applying Changes
|
|
61
|
+
|
|
62
|
+
Edit a definition, preview, then converge the stack:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bin/matsuri local_billing_dev diff stack billing # what would change
|
|
66
|
+
bin/matsuri local_billing_dev converge billing # apply it
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`show stack <name>` renders the full manifests without touching the
|
|
70
|
+
cluster.
|
|
71
|
+
|
|
72
|
+
For **billing**, a new image rides the converge positional:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bin/matsuri local_billing_dev converge billing v1.2.3
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For **checkout**, matsuri never carries the image: converge reapplies
|
|
79
|
+
only the declared fields, and the seeded trio stays with whatever
|
|
80
|
+
manager owns it. To play the deployment operator locally:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
kubectl --context matsuri -n checkout-dev \
|
|
84
|
+
set image deployment/checkout-web checkout=registry.acme.io/checkout:v2
|
|
85
|
+
bin/matsuri local_checkout_dev converge checkout # does not claw it back
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The staging environments (`billing_staging`, `checkout_staging`) are
|
|
89
|
+
illustrative — they name a cluster that does not exist — and add the
|
|
90
|
+
node-selection vocabulary the local environments leave out (see the
|
|
91
|
+
`case Matsuri.environment` in `platform/pods/`).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/bin/bash -e
|
|
2
|
+
|
|
3
|
+
# Pin the bundle to this repo's Gemfile and exec. That is the wrapper's
|
|
4
|
+
# entire job: the Gemfile owns which matsuri runs (see docs/design/boot.md
|
|
5
|
+
# in the matsuri repo). Invoke from the repo base.
|
|
6
|
+
#
|
|
7
|
+
# A per-environment entry point is argument injection, in a sibling script:
|
|
8
|
+
# exec "$(dirname "$0")/matsuri" staging "$@"
|
|
9
|
+
SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
|
|
10
|
+
PLATFORM_DIR="${SCRIPT_DIR}/.."
|
|
11
|
+
|
|
12
|
+
export BUNDLE_GEMFILE="${PLATFORM_DIR}/Gemfile"
|
|
13
|
+
|
|
14
|
+
exec bundle exec matsuri "$@"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Initializer for this repo
|
|
2
|
+
# This is where you load plugins and libraries specific to this repository
|
|
3
|
+
|
|
4
|
+
# Plugins
|
|
5
|
+
# require 'matsuri/traefik'
|
|
6
|
+
# require 'matsuri/helm'
|
|
7
|
+
|
|
8
|
+
# Local extensions
|
|
9
|
+
# Add into the local library. You can name it after
|
|
10
|
+
# your organization. Here, we'll use Acme
|
|
11
|
+
|
|
12
|
+
require 'acme'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Overrides Matsuri::Config
|
|
2
|
+
|
|
3
|
+
# Environment names are not kubectl context names here; each
|
|
4
|
+
# environment names its context in config/platform.rb
|
|
5
|
+
map_env_to_kube_context true
|
|
6
|
+
|
|
7
|
+
# Should not be storing secrets locally
|
|
8
|
+
# This is for demonstration only
|
|
9
|
+
default(:config_secrets_path) { File.join base_path, 'etc', 'secrets' }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# This sets up different Matsuri environments
|
|
2
|
+
#
|
|
3
|
+
# Environment names map to kubectl contexts via kube_context
|
|
4
|
+
# (map_env_to_kube_context is set in config/matsuri.rb). The local
|
|
5
|
+
# environments all point at the minikube "matsuri" profile, one
|
|
6
|
+
# namespace per stack.
|
|
7
|
+
|
|
8
|
+
config_context :local_app_dev do
|
|
9
|
+
default(:kube_context) { 'matsuri' }
|
|
10
|
+
default(:region) { 'local' }
|
|
11
|
+
default(:namespace) { 'app-dev' }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
config_context :local_billing_dev do
|
|
15
|
+
default(:kube_context) { 'matsuri' }
|
|
16
|
+
default(:region) { 'local' }
|
|
17
|
+
default(:namespace) { 'billing-dev' }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
config_context :local_checkout_dev do
|
|
21
|
+
default(:kube_context) { 'matsuri' }
|
|
22
|
+
default(:region) { 'local' }
|
|
23
|
+
default(:namespace) { 'checkout-dev' }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Staging contexts are illustrative: a real repo names the context the
|
|
27
|
+
# provider hands out (for GKE, let gcloud own authentication and
|
|
28
|
+
# naming).
|
|
29
|
+
config_context :billing_staging do
|
|
30
|
+
default(:kube_context) { 'gke_acme-platform_us-central1_staging' }
|
|
31
|
+
default(:region) { 'us-central1' }
|
|
32
|
+
default(:namespace) { 'billing' }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
config_context :checkout_staging do
|
|
36
|
+
default(:kube_context) { 'gke_acme-platform_us-central1_staging' }
|
|
37
|
+
default(:region) { 'us-central1' }
|
|
38
|
+
default(:namespace) { 'checkout' }
|
|
39
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Acme's Elixir workloads can efficiently use up the entire node.
|
|
2
|
+
# This mixins adds helpers for scheduling workloads on nodepools
|
|
3
|
+
# set aside for that
|
|
4
|
+
|
|
5
|
+
module Acme
|
|
6
|
+
module Nodes
|
|
7
|
+
module Dedicated
|
|
8
|
+
extend Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
let(:dedicated_key) { fail Matsuri::UndefinedLet, "Must define let(:dedicated_key)" }
|
|
12
|
+
|
|
13
|
+
let(:dedicated_node_selector) { { 'dedicated' => dedicated_key } }
|
|
14
|
+
let(:dedicated_toleration) do
|
|
15
|
+
tolerate \
|
|
16
|
+
key: 'dedicated',
|
|
17
|
+
operator: 'Equal',
|
|
18
|
+
value: dedicated_key,
|
|
19
|
+
effect: 'NoSchedule'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Acme set aside nodegroups that are preemptible
|
|
2
|
+
module Acme
|
|
3
|
+
module Nodes
|
|
4
|
+
module Preemptible
|
|
5
|
+
extend Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
let(:preemptible_toleration) do
|
|
9
|
+
tolerate \
|
|
10
|
+
key: 'preemptible',
|
|
11
|
+
operator: 'Exists',
|
|
12
|
+
effect: 'NoSchedule'
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Acme's Elixir workloads can efficiently use up the entire node.
|
|
2
|
+
# This mixins adds helpers for scheduling workloads on nodepools
|
|
3
|
+
# set aside for that
|
|
4
|
+
|
|
5
|
+
module Acme
|
|
6
|
+
module Nodes
|
|
7
|
+
module RequiredCores
|
|
8
|
+
extend Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
let(:required_cores) { fail Matsuri::UndefinedLet, "Must define let(:required_cores)" }
|
|
12
|
+
|
|
13
|
+
let(:cores_node_selector) { { 'cores' => required_cores } }
|
|
14
|
+
let(:cores_toleration) do
|
|
15
|
+
tolerate \
|
|
16
|
+
key: 'cores',
|
|
17
|
+
operator: 'Equal',
|
|
18
|
+
value: required_cores,
|
|
19
|
+
effect: 'NoSchedule'
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Acme's Elixir workloads can efficiently use up the entire node.
|
|
2
|
+
# This mixins adds helpers for scheduling workloads on nodepools
|
|
3
|
+
# set aside for that
|
|
4
|
+
|
|
5
|
+
module Acme
|
|
6
|
+
module Nodes
|
|
7
|
+
module ResourceClass
|
|
8
|
+
extend Concern
|
|
9
|
+
|
|
10
|
+
included do
|
|
11
|
+
let(:big_disk_toleration) { tolerate_resource_class('big-disk') }
|
|
12
|
+
let(:stateful) { tolerate_resource_class('stateful') }
|
|
13
|
+
let(:autopilot) { tolerate_resource_class('autopilot') }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def tolerate_resource_class(resource_class_key)
|
|
17
|
+
tolerate \
|
|
18
|
+
key: 'k8s.acme.io/resource-class',
|
|
19
|
+
operator: 'Equal',
|
|
20
|
+
value: resource_class_key,
|
|
21
|
+
effect: 'NoSchedule'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Acme CACerts
|
|
2
|
+
# Mount CACerts into a well-known directory
|
|
3
|
+
module Acme
|
|
4
|
+
module Pods
|
|
5
|
+
module CACerts
|
|
6
|
+
extend Concern
|
|
7
|
+
|
|
8
|
+
included do
|
|
9
|
+
let(:acme_cacerts_dir) { '/opt/acme/cacerts' }
|
|
10
|
+
let(:cacerts_mount) { mount('cacerts-volume', acme_cacerts_dir, read_only: true) }
|
|
11
|
+
let(:cacerts_volume) { config_map_volume('cacerts-volume', config_map_name: 'cacerts') }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Acme
|
|
2
|
+
module Pods
|
|
3
|
+
# Acme's standardized healthcheck for workloads
|
|
4
|
+
# Override specific let() for legacy workloads or workloads
|
|
5
|
+
# whose needs don't fit
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# # in a pod definition, add:
|
|
9
|
+
# include Acme::Pods::Healthcheck
|
|
10
|
+
module Healthcheck
|
|
11
|
+
extend Concern
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
let(:healthcheck_path) { '/healthcheck' }
|
|
15
|
+
# Standard Acme port. We can be fancier and read from Acme's standard web port
|
|
16
|
+
let(:healthcheck_port) { 8000 }
|
|
17
|
+
let(:healthcheck_initial_delay_seconds) { 7 }
|
|
18
|
+
let(:healthcheck_timeout_seconds) { 1 }
|
|
19
|
+
|
|
20
|
+
let(:readiness_probe) { healthcheck_probe }
|
|
21
|
+
let(:healthcheck_probe) do
|
|
22
|
+
{
|
|
23
|
+
httpGet: { path: healthcheck_path, port: healthcheck_port },
|
|
24
|
+
initialDelaySeconds: healthcheck_initial_delay_seconds,
|
|
25
|
+
timeoutSeconds: healthcheck_timeout_seconds,
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Acme's Elixir apps don't need liveness probes
|
|
30
|
+
# However, you can add a command liveness probe to check if it
|
|
31
|
+
# can still send commands to the running app
|
|
32
|
+
let(:liveness_probe) { nil }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Example local library
|
|
2
|
+
# This is the entry point for local concerns and customizations
|
|
3
|
+
# for Matsuri. You can also use this register your own
|
|
4
|
+
# custom resources, such as Istio VirtualService, or
|
|
5
|
+
# CloudNativePG resources
|
|
6
|
+
|
|
7
|
+
module Acme
|
|
8
|
+
module Pods
|
|
9
|
+
# It is better for teams to standardize on healthchecks. These would
|
|
10
|
+
# be promises (promise theory) that workloads make. The use of let()
|
|
11
|
+
# allows for overrides for specific workloads
|
|
12
|
+
autoload :Healthcheck, 'acme/pods/healthcheck'
|
|
13
|
+
autoload :CACerts, 'acme/pods/cacerts'
|
|
14
|
+
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module Nodes
|
|
18
|
+
# Scheduling
|
|
19
|
+
autoload :Dedicated, 'acme/nodes/dedicated'
|
|
20
|
+
autoload :Preemptible, 'acme/nodes/preemptible'
|
|
21
|
+
autoload :RequiredCores, 'acme/nodes/required_cores'
|
|
22
|
+
autoload :ResourceClass, 'acme/nodes/resource_class'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Acme's internal CA bundle, mounted by Acme::Pods::CACerts. A real
|
|
2
|
+
# repo would source this from the org's certificate distribution.
|
|
3
|
+
Matsuri.define :config_map, 'cacerts' do
|
|
4
|
+
let(:data) do
|
|
5
|
+
{
|
|
6
|
+
'acme-root.pem' => "-----BEGIN CERTIFICATE-----\nEXAMPLE\n-----END CERTIFICATE-----\n"
|
|
7
|
+
}
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Billing is stateless and reschedules cheaply, so it rides the
|
|
2
|
+
# preemptible pool where one exists. The image tracks the converge
|
|
3
|
+
# positional (default: the image_tag acceptor's default).
|
|
4
|
+
Matsuri.define :pod, 'billing' do
|
|
5
|
+
include Acme::Pods::Healthcheck
|
|
6
|
+
include Acme::Pods::CACerts
|
|
7
|
+
|
|
8
|
+
let(:labels) { { 'app' => 'billing' } }
|
|
9
|
+
|
|
10
|
+
# Node selection is per-environment: local clusters have no
|
|
11
|
+
# preemptible pool, so the scheduling vocabulary stays out entirely.
|
|
12
|
+
case Matsuri.environment
|
|
13
|
+
when 'billing_staging'
|
|
14
|
+
include Acme::Nodes::Preemptible
|
|
15
|
+
let(:tolerations) { [preemptible_toleration] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:cpu_limit) { '500m' }
|
|
19
|
+
let(:mem_limit) { '512Mi' }
|
|
20
|
+
|
|
21
|
+
let(:volumes) { [cacerts_volume] }
|
|
22
|
+
let(:container) do
|
|
23
|
+
{
|
|
24
|
+
name: 'billing',
|
|
25
|
+
image: "registry.acme.io/billing:#{image_tag}",
|
|
26
|
+
readinessProbe: readiness_probe,
|
|
27
|
+
resources: resources,
|
|
28
|
+
volumeMounts: [cacerts_mount]
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|