Repository navigation
Expand file tree
/
Copy pathMakefile.k8s
More file actions
339 lines (304 loc) · 14.7 KB
/
Copy pathMakefile.k8s
File metadata and controls
339 lines (304 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# The Kubernetes shape of the lab.
#
# `make up` remains the compose stack: one command, ninety seconds, no
# cluster. This file is the second shape — the same source, deployed the way
# it would actually run, with the parties separated by a service mesh and the
# authorization server replicated behind a database.
#
# Both cannot run at once: they both want the host's :443 and :53.
KIND_CLUSTER := uma4agents
K8S := k8s
NS_AGENT := sterling-vance
## kind-up: the whole Kubernetes lab, from nothing
.PHONY: kind-up
kind-up: kind-preflight kind-cluster k8s-registry k8s-platform k8s-build k8s-apply k8s-wait
@# In a Codespace the portal is not reachable until the forwards exist and
@# the OIDC origins name the forwarded host. Doing that here rather than
@# leaving it as a step to remember: the ports are listed in the PORTS tab
@# from container start, so anything less makes them look ready when they
@# are not.
@if [ -n "$$CODESPACE_NAME" ] || [ -d /workspaces/.codespaces ]; then \
bash .devcontainer/expose-web.sh; \
fi
@echo
@echo " The lab is up. Next:"
@echo " make k8s-smoke-test verify it end to end"
@echo " make k8s-demo-all walk Alice's day"
@echo " make k8s-policy-test prove the boundary denies"
@echo
@if [ -z "$$CODESPACE_NAME" ] && [ ! -d /workspaces/.codespaces ]; then \
echo " In a browser: https://portal.uma.lab (alice / alice-demo)"; \
echo " Needs 'make dns-setup' once, and 'make k8s-trust-ca' for a green padlock."; \
fi
## kind-preflight: fail early and legibly rather than half-way through
.PHONY: kind-preflight
kind-preflight:
@missing=""; \
for t in docker kind kubectl helm; do \
command -v $$t >/dev/null 2>&1 || missing="$$missing $$t"; \
done; \
if [ -n "$$missing" ]; then \
echo "Missing:$$missing"; \
echo " brew install kind helm (docker and kubectl come with Docker Desktop)"; \
exit 1; \
fi
@if [ -n "$$(docker compose ps -q 2>/dev/null)" ]; then \
echo "The compose stack is running and holds :443 and :53."; \
echo "Run 'make down' first — the two shapes cannot share a machine."; \
exit 1; \
fi
## kind-cluster: create the local cluster (three nodes)
.PHONY: kind-cluster
kind-cluster:
@kind get clusters 2>/dev/null | grep -qx $(KIND_CLUSTER) \
|| kind create cluster --config $(K8S)/kind/cluster.yaml
## kind-down: delete the cluster and the registry beside it
.PHONY: kind-down
kind-down:
@kind delete cluster --name $(KIND_CLUSTER) 2>/dev/null || true
@docker rm -f u4a-registry >/dev/null 2>&1 || true
@echo "cluster and registry removed"
.PHONY: k8s-registry
k8s-registry:
@echo "==> Registry"
@$(K8S)/scripts/registry.sh
## k8s-platform: install everything the lab sits on (pinned in versions.env)
.PHONY: k8s-platform
k8s-platform:
@$(K8S)/platform/install.sh
## k8s-build: build the lab's images and push them to the local registry
.PHONY: k8s-build
k8s-build: fetch-upstream
@echo
@echo "==> Images"
@$(K8S)/scripts/build-images.sh
## k8s-apply: the lab itself — namespaces, workloads, routes, policy
.PHONY: k8s-apply
k8s-apply:
@echo
@echo "==> The lab"
@kubectl apply -f $(K8S)/base/namespaces/ >/dev/null
@kubectl apply -f $(K8S)/base/edge/ca.yaml >/dev/null
@kubectl -n cert-manager wait --for=condition=Ready certificate/uma-lab-ca --timeout=120s >/dev/null
@kubectl apply -f $(K8S)/base/edge/ >/dev/null
@$(K8S)/scripts/coredns-patch.sh
@kubectl -n alice create configmap alice-realm \
--from-file=alice-realm.json=keycloak/alice-realm.json \
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
@kubectl apply -f $(K8S)/base/alice/ >/dev/null
@kubectl apply -f $(K8S)/base/meridian/ >/dev/null
@kubectl apply -f $(K8S)/base/sterling-vance/ >/dev/null
@kubectl apply -f $(K8S)/base/aauth/ >/dev/null
@kubectl apply -f $(K8S)/base/policy/waypoints.yaml >/dev/null
@kubectl -n alice wait --for=condition=Programmed gateway/waypoint --timeout=180s >/dev/null
@kubectl -n meridian wait --for=condition=Programmed gateway/waypoint --timeout=180s >/dev/null
@kubectl apply -f $(K8S)/base/policy/ >/dev/null
@$(K8S)/scripts/job-configmaps.sh
@echo " applied"
## k8s-wait: block until the lab is actually usable
.PHONY: k8s-wait
k8s-wait:
@echo
@echo "==> Waiting for the lab"
@kubectl -n alice rollout status deploy/uma-as --timeout=300s >/dev/null
@kubectl -n alice rollout status deploy/keycloak --timeout=300s >/dev/null
@kubectl -n alice rollout status deploy/alice-portal --timeout=300s >/dev/null
@kubectl -n meridian rollout status deploy/uma-pep --timeout=300s >/dev/null
@kubectl -n meridian rollout status deploy/alice-vault --timeout=300s >/dev/null
@kubectl -n sterling-vance rollout status deploy/agent-operator --timeout=300s >/dev/null
@kubectl -n aauth rollout status statefulset/person-server --timeout=300s >/dev/null
@printf " waiting for the declarative pull to land"
@for i in $$(seq 1 60); do \
if kubectl -n alice exec deploy/uma-as -- \
python3 -c "import urllib.request;urllib.request.urlopen('http://localhost:9000/health/registry')" \
>/dev/null 2>&1; then echo " ok"; exit 0; fi; \
printf "."; sleep 3; \
done; echo; echo " the registry pull has not landed — see 'kubectl -n alice logs -l app=uma-as'"
# ---------------------------------------------------------------------------
# Verification
# ---------------------------------------------------------------------------
# Every check runs as a Job inside the cluster, in the requesting party's
# namespace. That is not only convenient — it is the honest place to stand:
# *.uma.lab resolves through CoreDNS and the lab CA is already mounted, so
# nothing needs host DNS, a trusted certificate, or sudo, and the checks
# reach Alice's resources the only way Bob's side can.
define run_job
@kubectl -n $(NS_AGENT) delete job $(1) --ignore-not-found >/dev/null 2>&1
@$(2) | kubectl apply -f - >/dev/null
@kubectl -n $(NS_AGENT) wait --for=condition=Complete --for=condition=Failed \
job/$(1) --timeout=$(3) >/dev/null 2>&1 || true
@kubectl -n $(NS_AGENT) logs job/$(1) 2>/dev/null
@test "$$(kubectl -n $(NS_AGENT) get job $(1) -o jsonpath='{.status.succeeded}')" = "1"
endef
## k8s-smoke-test: verify the deployed lab end to end
.PHONY: k8s-smoke-test
k8s-smoke-test:
$(call run_job,smoke-test,cat $(K8S)/base/jobs/smoke-test.yaml,300s)
## k8s-demo-all: walk Alice's day (ACT=tier1|tier2|tier3|revocation|all)
##
## SIM=0 leaves her taps to you: the ask-me act then pends in her portal
## until you approve it there, which is the whole point of the shape and
## cannot be seen while something else is tapping for her.
.PHONY: k8s-demo-all
k8s-demo-all: ACT ?= all
k8s-demo-all: SIM ?= 1
k8s-demo-all:
$(call run_job,demo,sed -e 's|--act all|--act $(ACT)|' \
$(if $(filter 0,$(SIM)),-e 's| --simulate-alice||',) \
$(K8S)/base/jobs/demo.yaml,600s)
## k8s-policy-test: prove the trust boundary denies what it claims to
.PHONY: k8s-policy-test
k8s-policy-test:
$(call run_job,policy-test,cat $(K8S)/base/jobs/policy-test.yaml,300s)
## k8s-assurance-check: agent assurance and the owner's attention budget,
## proved through the edge from the requesting party's namespace.
.PHONY: k8s-assurance-check
k8s-assurance-check:
$(call run_job,assurance-check,cat $(K8S)/base/jobs/assurance-check.yaml,420s)
## k8s-intent-check: whose intent the grant carries — her terms, which cannot
## be signed weaker than she wrote them, and the agent's own account of the
## errand, recorded and never judged. Worth running here as well as under
## compose: the trajectory a rule reads is a query against a shared table, so
## this is where "the same count whichever replica answered" stops being an
## assumption. See docs/INTENT.md.
.PHONY: k8s-intent-check
k8s-intent-check:
$(call run_job,intent-check,cat $(K8S)/base/jobs/intent-check.yaml,420s)
## kagent: an agent framework nobody modified, governed by Alice's policy.
##
## Off by default because it brings a model with it, and that is a cost nobody
## should pay for a lab they only wanted to read. Two ways to supply one:
##
## make kagent a model in the cluster, no account
## anywhere (ollama, pulled on start)
## make kagent MODEL=anthropic ANTHROPIC_API_KEY from your environment
## make kagent MODEL=openai OPENAI_API_KEY from your environment
##
## Either way the U4A path is identical: the adapter holds Bob's key and runs
## the four beats, and the model only decides which tool to call.
.PHONY: kagent kagent-down kagent-check
kagent: MODEL ?= ollama
kagent:
@./k8s/scripts/kagent.sh up "$(MODEL)"
kagent-down:
@./k8s/scripts/kagent.sh down
## kagent-check: ask it a question and watch Alice's policy decide
kagent-check: MODEL ?= ollama
kagent-check: kagent
@./k8s/scripts/kagent.sh check
## k8s-adapter: the U4A adapter as a service in Bob's namespace. Off by
## default, like every other opt-in piece, so a cold cluster stays the portal
## demo. `make kagent` brings it up too.
.PHONY: k8s-adapter k8s-adapter-down
k8s-adapter:
@./k8s/scripts/job-configmaps.sh
@kubectl -n sterling-vance scale deploy/agent-shim --replicas=1 >/dev/null
@kubectl -n sterling-vance rollout status deploy/agent-shim --timeout=300s >/dev/null
@echo "==> adapter up at agent-shim.sterling-vance:9030/mcp"
k8s-adapter-down:
@kubectl -n sterling-vance scale deploy/agent-shim --replicas=0 >/dev/null
@echo "==> adapter stopped"
## k8s-adapter-check: the same claim as `make adapter-check`, in the cluster —
## an MCP client with no U4A code in it, reaching Alice's things.
.PHONY: k8s-adapter-check
k8s-adapter-check: k8s-adapter
$(call run_job,adapter-check,cat $(K8S)/base/jobs/adapter-check.yaml,300s)
## k8s-load: concurrency against the deployed shape, not against the store
# 24 agents negotiating at once through the edge, against three authorization
# servers sharing one database. Asserts every ticket is distinct, that a
# ticket presented by many at once is answered exactly once, and that the
# replicas are indistinguishable.
.PHONY: k8s-load
k8s-load:
$(call run_job,load-test,cat $(K8S)/base/jobs/load-test.yaml,300s)
## k8s-chaos: kill things while Alice is being asked
# Puts a request in front of her, deletes the authorization server that took
# it, then fails the database over — and shows the pend surviving both. The
# premise of the whole demo is that the owner may be asleep for hours; this
# is what makes that a property of the deployment rather than of the protocol
# alone.
.PHONY: k8s-chaos
k8s-chaos:
@./k8s/scripts/chaos.sh
## k8s-audit: print Alice's ledger from the deployed lab
.PHONY: k8s-audit
k8s-audit:
@kubectl -n alice exec deploy/alice-portal -- python3 -c "\
import json,os,urllib.request,urllib.parse;\
tok=json.load(urllib.request.urlopen(urllib.request.Request(\
'https://keycloak.uma.lab/realms/alice/protocol/openid-connect/token',\
data=urllib.parse.urlencode({'grant_type':'password','client_id':'alice-portal',\
'username':'alice','password':'alice-demo'}).encode()),\
cafile='/certs/rootCA.pem'))['access_token'];\
r=urllib.request.urlopen(urllib.request.Request(\
'http://uma-as.alice.svc.cluster.local:9000/owner/ledger',\
headers={'Authorization':'Bearer '+tok}));\
print(json.dumps(json.load(r),indent=2))"
## k8s-paios: scale up her personal AI, so it answers instead of her portal.
## Off by default for the same reason as compose: while it runs, the tiers it
## has standing consent for are already answered and her portal has nothing
## waiting. Run `make k8s-paios-down` before the portal walkthrough.
.PHONY: k8s-paios k8s-paios-down k8s-paios-check
k8s-paios:
@kubectl -n alice scale deployment/paios --replicas=1 >/dev/null
@kubectl -n alice rollout status deployment/paios --timeout=180s
@echo "==> Her personal AI is answering. 'make k8s-paios-down' hands it back to her portal."
k8s-paios-down:
@kubectl -n alice scale deployment/paios --replicas=0 >/dev/null
@echo "==> Stopped. Pending requests wait for her portal again."
## k8s-paios-check: the same check the compose path runs, in the cluster.
k8s-paios-check: k8s-paios
@kubectl -n alice exec deploy/paios -- python /opt/u4a/paios_check.py
## k8s-reset: rewind the story without rebuilding the cluster
.PHONY: k8s-reset
k8s-reset:
@kubectl -n alice exec uma-as-db-1 -- psql -U postgres -d u4a -q -c \
"TRUNCATE tickets, negotiations, rpts, connections, ledger, owner_events;" >/dev/null
@kubectl -n alice rollout restart deploy/uma-as >/dev/null
@kubectl -n alice rollout status deploy/uma-as --timeout=180s >/dev/null
@echo "grant state cleared; the tiers and terms Alice authored are kept"
## k8s-trust-ca: trust the cluster's CA locally, for a green padlock
.PHONY: k8s-trust-ca
k8s-trust-ca:
@kubectl -n cert-manager get secret uma-lab-ca -o jsonpath='{.data.ca\.crt}' \
| base64 -d > /tmp/u4a-k8s-ca.pem
@echo "CA written to /tmp/u4a-k8s-ca.pem"
@echo "macOS: sudo security add-trusted-cert -d -r trustRoot \\"
@echo " -k /Library/Keychains/System.keychain /tmp/u4a-k8s-ca.pem"
## k8s-verify-extauth: prove the ext_authz contract survives config file -> CRDs
# The enforcement core decides from request facts. Under the file-driven
# gateway those facts arrive because of three settings in gateway/agw.yaml;
# here the same three exist under different names on a different resource.
# Asserts what reaches the authorization service is unchanged — including the
# one header that does not survive, so a release cannot change it silently.
.PHONY: k8s-verify-extauth
k8s-verify-extauth:
@./k8s/verify/extauth/run.sh
## k8s-status: what is running, and where the seams are
.PHONY: k8s-status
k8s-status:
@for ns in uma-edge alice meridian sterling-vance aauth; do \
printf '\n\033[1m%s\033[0m %s\n' "$$ns" \
"$$(kubectl get ns $$ns -o jsonpath='{.metadata.annotations.u4a\.ai/description}' 2>/dev/null)"; \
kubectl -n $$ns get pods --no-headers 2>/dev/null | awk '{printf " %-34s %s\n", $$1, $$3}'; \
done
## codespaces-web: publish Alice's portal to a browser tab in a Codespace
##
## Only meaningful inside a Codespace. The lab routes by hostname under
## *.uma.lab, which a browser outside the VM cannot resolve; this forwards
## the portal and her identity provider directly and rewrites the three
## OIDC origins that would otherwise still name keycloak.uma.lab.
.PHONY: codespaces-web
codespaces-web:
@bash .devcontainer/expose-web.sh
## k8s-many-agents: N distinct agents, N connections, one policy (AGENTS=12)
##
## Not part of any suite yet — written, never run. The assertions are the
## interesting ones (distinct keys become distinct connections, revoking one
## leaves the rest, her policy still names no agent), but nothing has proved
## the script itself is right, so treat a failure as suspect until it has
## passed once.
.PHONY: k8s-many-agents
k8s-many-agents: AGENTS ?= 12
k8s-many-agents:
$(call run_job,many-agents,sed 's|"12"|"$(AGENTS)"|' $(K8S)/base/jobs/many-agents.yaml,600s)