Add an on/off switch for the MAS facility.
MAS-WP-0007 is finished. power.sh scales the MX and statute together and records the power annotation. apply.sh keeps an off facility off. The live facility was annotated power=on and was not restarted. Assistant: grok Assistant-Session: 01a198fa-fd27-7e41-a0a2-57355c3e4efd
This commit is contained in:
parent
b108e89af4
commit
f18eb0b8df
12 changed files with 344 additions and 36 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# MAS live-test MX for mas.coulomb.social (MAS-WP-0003).
|
||||
# MAS facility image for mas.coulomb.social (MAS-WP-0007).
|
||||
# Build from the repo root: docker build -f deploy/railiance/Dockerfile .
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
|
|
|
|||
37
deploy/railiance/README.md
Normal file
37
deploy/railiance/README.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# MAS facility
|
||||
|
||||
The MX and the statute for `mas.coulomb.social`. Replies go out through
|
||||
Scaleway TEM and stay capped at 300. MAS-WP-0007.
|
||||
|
||||
## On and off
|
||||
|
||||
`power.sh` scales `deploy/mas-mx` and `deploy/mas-statute` together. It
|
||||
writes annotation `mas.coulomb.social/power` on namespace `mas`.
|
||||
|
||||
| Power | What runs | What stays |
|
||||
|---|---|---|
|
||||
| `on` | both Deployments at 1 replica | evidence PVC, Services, Ingress, ExternalSecret, DNS, OpenBao role |
|
||||
| `off` | both Deployments at 0 | the same durable parts; port 25 has no ready pod |
|
||||
|
||||
```bash
|
||||
KUBECONFIG=~/.kube/config-railiance01 deploy/railiance/power.sh status
|
||||
KUBECONFIG=~/.kube/config-railiance01 deploy/railiance/power.sh off
|
||||
KUBECONFIG=~/.kube/config-railiance01 deploy/railiance/power.sh on
|
||||
```
|
||||
|
||||
The workstation's default kubeconfig is `ops-mason-builder`. That identity
|
||||
can see Services and cannot scale these Deployments. Use the railiance01
|
||||
admin kubeconfig for `power.sh` and `apply.sh`.
|
||||
|
||||
`apply.sh IMAGE` renders `manifests.yaml` and applies it. When the
|
||||
annotation is `off`, both Deployments are rendered at 0 replicas, so a
|
||||
later apply does not turn the facility back on. A missing annotation means
|
||||
on. The script stops if it cannot read the namespace.
|
||||
|
||||
## Limits
|
||||
|
||||
One replica. The image is side-loaded into k3s containerd, not pulled from
|
||||
a registry. SMTP TLS is the image's snakeoil certificate. The running image
|
||||
is `docker.io/library/mas-live:v0.2-480e69a`, whose banner still says
|
||||
`MAS live test`. These stay until a later decision. This switch does not
|
||||
roll a new image.
|
||||
23
deploy/railiance/apply.sh
Executable file
23
deploy/railiance/apply.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/sh
|
||||
# Apply the facility manifest without undoing an off switch (MAS-WP-0007).
|
||||
# A missing power annotation is on. Refuses to apply when the namespace
|
||||
# annotation cannot be read.
|
||||
set -eu
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: apply.sh IMAGE" >&2
|
||||
exit 2
|
||||
fi
|
||||
image=$1
|
||||
|
||||
if ! power=$(kubectl get namespace mas \
|
||||
-o jsonpath='{.metadata.annotations.mas\.coulomb\.social/power}'); then
|
||||
echo "cannot read namespace mas; refusing to apply" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$power" ]; then
|
||||
power=on
|
||||
fi
|
||||
|
||||
python3 ./facility.py render "$image" "$power" < manifests.yaml | kubectl apply -f -
|
||||
81
deploy/railiance/facility.py
Normal file
81
deploy/railiance/facility.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"""Render the MAS facility manifest for a power state.
|
||||
|
||||
This file does not talk to the cluster. power.sh and apply.sh do.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
SWITCHABLE = ("mas-mx", "mas-statute")
|
||||
|
||||
|
||||
def render(manifest: str, image: str, power: str) -> str:
|
||||
"""Return the manifest with IMAGE filled in and replicas set from power.
|
||||
|
||||
`on` keeps one replica of each switchable Deployment. `off` sets both
|
||||
to zero. Services, the evidence volume, the Ingress, and the
|
||||
ExternalSecret are copied through unchanged.
|
||||
"""
|
||||
if power not in ("on", "off"):
|
||||
raise ValueError(f"power must be on or off, got {power!r}")
|
||||
if not image or any(ch.isspace() for ch in image):
|
||||
raise ValueError("image must be a single token")
|
||||
if "image: IMAGE" not in manifest:
|
||||
raise ValueError("manifest has no image: IMAGE placeholder")
|
||||
|
||||
rendered = manifest.replace("image: IMAGE", f"image: {image}")
|
||||
docs = rendered.split("\n---\n")
|
||||
seen: set[str] = set()
|
||||
out: list[str] = []
|
||||
for doc in docs:
|
||||
name = _deployment_name(doc)
|
||||
if name in SWITCHABLE:
|
||||
seen.add(name)
|
||||
doc = _set_replicas(doc, 0 if power == "off" else 1)
|
||||
out.append(doc)
|
||||
missing = [name for name in SWITCHABLE if name not in seen]
|
||||
if missing:
|
||||
raise ValueError("manifest is missing Deployments: " + ", ".join(missing))
|
||||
return "\n---\n".join(out)
|
||||
|
||||
|
||||
def _deployment_name(doc: str) -> str | None:
|
||||
kind = None
|
||||
name = None
|
||||
for line in doc.splitlines():
|
||||
if line.startswith("kind:"):
|
||||
kind = line.split(":", 1)[1].strip()
|
||||
elif line.startswith(" name:") and name is None:
|
||||
name = line.split(":", 1)[1].strip()
|
||||
if kind == "Deployment":
|
||||
return name
|
||||
return None
|
||||
|
||||
|
||||
def _set_replicas(doc: str, count: int) -> str:
|
||||
lines = doc.splitlines(keepends=True)
|
||||
found = 0
|
||||
out = []
|
||||
for line in lines:
|
||||
if line.startswith(" replicas:"):
|
||||
found += 1
|
||||
newline = "\n" if line.endswith("\n") else ""
|
||||
out.append(f" replicas: {count}{newline}")
|
||||
else:
|
||||
out.append(line)
|
||||
if found != 1:
|
||||
raise ValueError(f"expected one replicas field, found {found}")
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if len(argv) != 4 or argv[1] != "render":
|
||||
print("usage: facility.py render IMAGE on|off", file=sys.stderr)
|
||||
return 2
|
||||
sys.stdout.write(render(sys.stdin.read(), argv[2], argv[3]))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
# MAS live provider test (mail-address-semantics MAS-WP-0003).
|
||||
# Founder go for A-D: 2026-10-08. Temporary: delete the namespace after the test.
|
||||
# A namespace + MX pod B LoadBalancer :25 C statute-only ingress D no outbound mail
|
||||
# D superseded 2026-10-18: operator chose replies via Scaleway TEM (Secret mas-relay,
|
||||
# synced by ExternalSecret from OpenBao platform/suppliers/scaleway/mas-tem-relay).
|
||||
# IMAGE is replaced with the pushed digest at apply time.
|
||||
# MAS facility for mas.coulomb.social (MAS-WP-0007).
|
||||
# Durable: this namespace, the evidence PVC, Services, Ingress, ExternalSecret.
|
||||
# Switchable: Deployments mas-mx and mas-statute (replicas 1 on, 0 off).
|
||||
# deploy/railiance/apply.sh reads annotation mas.coulomb.social/power and
|
||||
# renders replicas 0 when the facility is off. A missing annotation means on.
|
||||
# Replies use Scaleway TEM (Secret mas-relay, from OpenBao
|
||||
# platform/suppliers/scaleway/mas-tem-relay), capped at 300.
|
||||
# IMAGE is replaced at apply time. The running image is side-loaded.
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
|
|
@ -20,8 +22,8 @@ spec:
|
|||
---
|
||||
# TEM SMTP login from OpenBao (ops-mason MASON-WP-0014, CCR-2026-0051).
|
||||
# OpenBao role external-secrets-mas-tem-relay and its read policy are
|
||||
# cluster-scoped. Retire them at MAS-WP-0003 close; deleting the namespace
|
||||
# does not remove them.
|
||||
# cluster-scoped. They stay with the facility. Deleting the namespace would
|
||||
# not remove them, and the power switch does not delete the namespace.
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata: {name: mas-tem-relay-eso, namespace: mas}
|
||||
|
|
|
|||
73
deploy/railiance/power.sh
Executable file
73
deploy/railiance/power.sh
Executable file
|
|
@ -0,0 +1,73 @@
|
|||
#!/bin/bash
|
||||
# Turn the MAS facility on or off (MAS-WP-0007).
|
||||
# Scales only deploy/mas-mx and deploy/mas-statute. The namespace, evidence
|
||||
# PVC, Services, Ingress, and ExternalSecret stay.
|
||||
# Records the desired power on namespace mas:
|
||||
# mas.coulomb.social/power=on|off
|
||||
set -eu
|
||||
set -o pipefail
|
||||
|
||||
ns=mas
|
||||
annotation=mas.coulomb.social/power
|
||||
deployments="mas-mx mas-statute"
|
||||
|
||||
usage() {
|
||||
echo "usage: power.sh on|off|status" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
power_now() {
|
||||
kubectl get namespace "$ns" \
|
||||
-o jsonpath='{.metadata.annotations.mas\.coulomb\.social/power}'
|
||||
}
|
||||
|
||||
wait_ready() {
|
||||
name=$1
|
||||
kubectl -n "$ns" rollout status "deploy/$name" --timeout=180s
|
||||
}
|
||||
|
||||
wait_stopped() {
|
||||
name=$1
|
||||
i=0
|
||||
while [ "$i" -lt 90 ]; do
|
||||
left=$(kubectl -n "$ns" get pods -l "app.kubernetes.io/name=$name" --no-headers 2>/dev/null | wc -l)
|
||||
left=$(echo "$left" | tr -d ' ')
|
||||
if [ "$left" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
i=$((i + 1))
|
||||
sleep 2
|
||||
done
|
||||
echo "pods for $name are still running" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cmd=${1:-}
|
||||
case "$cmd" in
|
||||
on|off)
|
||||
kubectl annotate namespace "$ns" "$annotation=$cmd" --overwrite
|
||||
if [ "$cmd" = on ]; then
|
||||
replicas=1
|
||||
else
|
||||
replicas=0
|
||||
fi
|
||||
for name in $deployments; do
|
||||
kubectl -n "$ns" scale "deploy/$name" --replicas="$replicas"
|
||||
done
|
||||
for name in $deployments; do
|
||||
if [ "$cmd" = on ]; then
|
||||
wait_ready "$name"
|
||||
else
|
||||
wait_stopped "$name"
|
||||
fi
|
||||
done
|
||||
;;
|
||||
status)
|
||||
echo "power=$(power_now)"
|
||||
kubectl -n "$ns" get deploy $deployments
|
||||
kubectl -n "$ns" get endpointslice -l 'kubernetes.io/service-name in (mas-smtp,mas-statute)'
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
Loading…
Add table
Add a link
Reference in a new issue