forked from google/sam
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontainer_mesh.bash
More file actions
509 lines (453 loc) · 18.3 KB
/
Copy pathcontainer_mesh.bash
File metadata and controls
509 lines (453 loc) · 18.3 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
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
#!/usr/bin/env bash
# Shared BATS helpers for containerized SAM mesh tests.
# Refactored to use Kind-hosted unified OIDC and control plane services.
if [[ -z "${MESH_HELPERS_LOADED:-}" ]]; then
MESH_HELPERS_LOADED=1
MESH_RUNTIME_IMAGE="${MESH_RUNTIME_IMAGE:-sam-e2e-runtime:local}"
MESH_NETWORK="kind"
MESH_CONTAINERS=()
MESH_PREFIX=""
MESH_SOCKET_DIR=""
mesh_cleanup_stale_resources() {
local stale_containers
stale_containers=$(docker ps -aq --filter "name=mesh-")
if [[ -n "${stale_containers}" ]]; then
docker rm -f ${stale_containers} >/dev/null 2>&1 || true
fi
}
mesh_require_docker() {
command -v docker >/dev/null 2>&1 || return 1
docker info >/dev/null 2>&1 || return 1
return 0
}
mesh_build_runtime_image() {
if ! docker image inspect "${MESH_RUNTIME_IMAGE}" >/dev/null 2>&1; then
docker build \
-f tests/e2e/docker/Dockerfile.sam-runtime \
-t "${MESH_RUNTIME_IMAGE}" \
. >/dev/null
fi
}
mesh_setup_env() {
if [[ -n "${MESH_PREFIX:-}" ]]; then
return 0
fi
mesh_build_runtime_image
MESH_PREFIX="mesh-${BATS_TEST_NUMBER}-$$-$(date +%s)"
MESH_SOCKET_DIR="/tmp/${MESH_PREFIX}-sockets"
mkdir -p "${MESH_SOCKET_DIR}"
CLEANUP_VOLUMES=()
}
mesh_cleanup_test_resources() {
if [[ "${BATS_TEST_COMPLETED:-0}" -ne 1 ]]; then
mkdir -p tests/e2e/logs
local c
for c in "${MESH_CONTAINERS[@]}"; do
docker logs "${c}" > "tests/e2e/logs/${c}.log" 2>&1 || true
done
fi
local c
for c in "${MESH_CONTAINERS[@]}"; do
docker rm -f "${c}" >/dev/null 2>&1 || true
done
MESH_CONTAINERS=()
local v
for v in "${CLEANUP_VOLUMES[@]}"; do
docker volume rm "${v}" >/dev/null 2>&1 || true
done
CLEANUP_VOLUMES=()
}
mesh_cleanup_env() {
mesh_cleanup_test_resources
}
mesh_gen_hex32() {
hexdump -vn 32 -e '1/1 "%02x"' /dev/urandom
}
mesh_wait_for_log() {
local container="$1"
local needle="$2"
local timeout_s="${3:-20}"
local i
for ((i=0; i<timeout_s*10; i++)); do
if docker logs "${container}" 2>&1 | grep -Fq "${needle}"; then
return 0
fi
sleep 0.1
done
return 1
}
# Pods matching a selector that will never become ready on their own.
# Deliberately excludes CrashLoopBackOff: the control plane restarts a few
# times while the database comes up, so treating that as terminal would swap
# one flake for another. Images are preloaded with `kind load`, so a pull
# failure is always a real one.
mesh_unrecoverable_pods() {
local selector="$1"
[[ -n "${selector}" ]] || return 0
kubectl --context="${KUBECONTEXT}" get pods -l "${selector}" \
-o jsonpath='{range .items[*]}{.metadata.name}{"="}{range .status.containerStatuses[*]}{.state.waiting.reason}{end}{"\n"}{end}' 2>/dev/null |
grep -E '=(ImagePullBackOff|InvalidImageName|CreateContainerConfigError)$' || true
}
# Scoped so an unrelated broken pod elsewhere in the namespace cannot abort a
# wait for a healthy workload.
mesh_selector_for() {
local target="$1" selector
selector=$(kubectl --context="${KUBECONTEXT}" get "${target}" \
-o go-template='{{range $k, $v := .spec.selector.matchLabels}}{{$k}}={{$v}},{{end}}' 2>/dev/null) || return 0
echo "${selector%,}"
}
mesh_dump_cluster() {
local what="$1"
{
echo "--- ${what} never became ready ---"
kubectl --context="${KUBECONTEXT}" get pods -o wide
kubectl --context="${KUBECONTEXT}" get events --sort-by=.lastTimestamp | tail -30
local pod
for pod in $(kubectl --context="${KUBECONTEXT}" get pods -o name); do
echo "--- ${pod} ---"
kubectl --context="${KUBECONTEXT}" logs "${pod}" --all-containers --tail=50 2>&1 || true
done
} >&2 2>&1 || true
}
# Waits for a workload without putting a stopwatch on a busy machine.
#
# The ceiling is generous, which costs nothing when things are healthy because
# this returns the moment the rollout completes; the old fixed 60s failed runs
# for being slow rather than broken, on a cold cluster that had just loaded
# four images onto three nodes. A pod that cannot recover still aborts at once,
# so a genuine breakage is no slower to report than before, and now says why.
mesh_wait_for_rollout() {
local target="$1"
local timeout_s="${2:-${MESH_ROLLOUT_TIMEOUT:-300}}"
local deadline=$((SECONDS + timeout_s))
local selector
selector="$(mesh_selector_for "${target}")"
while true; do
# Doubles as the poll interval: it blocks until ready or the slice expires.
if kubectl --context="${KUBECONTEXT}" rollout status "${target}" --timeout=5s >/dev/null 2>&1; then
return 0
fi
local stuck
stuck="$(mesh_unrecoverable_pods "${selector}")"
if [[ -n "${stuck}" ]]; then
echo "${target}: pod cannot recover: ${stuck}" >&2
mesh_dump_cluster "${target}"
return 1
fi
if ((SECONDS >= deadline)); then
echo "${target}: not ready after ${timeout_s}s" >&2
mesh_dump_cluster "${target}"
return 1
fi
done
}
mesh_wait_for_job() {
local target="$1"
local timeout_s="${2:-${MESH_ROLLOUT_TIMEOUT:-300}}"
local deadline=$((SECONDS + timeout_s))
while true; do
if kubectl --context="${KUBECONTEXT}" wait --for=condition=complete --timeout=5s "${target}" >/dev/null 2>&1; then
return 0
fi
if kubectl --context="${KUBECONTEXT}" wait --for=condition=failed --timeout=1s "${target}" >/dev/null 2>&1; then
echo "${target}: failed" >&2
mesh_dump_cluster "${target}"
return 1
fi
if ((SECONDS >= deadline)); then
echo "${target}: did not complete within ${timeout_s}s" >&2
mesh_dump_cluster "${target}"
return 1
fi
done
}
mesh_wait_for_mcp_ready() {
local idx="$1"
local timeout_s="${2:-20}"
local i
for ((i=0; i<timeout_s; i++)); do
if docker run --rm --network "${MESH_NETWORK}" python:3.12 curl -s -X POST -H "Content-Type: application/json" -H "X-Sam-Authentication: Bearer secret-token" -d '{"jsonrpc":"2.0","method":"ping","id":1}' --max-time 5 -D - http://${MESH_PREFIX}-node-${idx}:8080/mcp | grep -q "200 OK"; then
return 0
fi
sleep 1
done
return 1
}
mesh_get_node_count_via_mcp() {
local idx="$1"
local output
output="$(timeout 15s docker run --rm --network "${MESH_NETWORK}" "${MESH_RUNTIME_IMAGE}" mcp-client -url "http://${MESH_PREFIX}-node-${idx}:8080/mcp" -tool "get_mesh_info" 2>/dev/null)"
echo "${output}" | jq 'if .connected_peers then (.connected_peers | length) - 1 else 0 end'
}
mesh_wait_for_node_count() {
local idx="$1"
local expected="$2"
local timeout_s="${3:-20}"
local i
for ((i=0; i<timeout_s; i++)); do
local output
output="$(timeout 15s docker run --rm --network "${MESH_NETWORK}" "${MESH_RUNTIME_IMAGE}" mcp-client -url "http://${MESH_PREFIX}-node-${idx}:8080/mcp" -tool "get_mesh_info" 2>/dev/null)"
echo "Node ${idx} get_mesh_info raw output: ${output}"
local count
count="$(echo "${output}" | jq 'if .connected_peers then (.connected_peers | length) - 1 else 0 end')"
echo "Node ${idx} reported known peers count: ${count}"
if [[ "${count}" -eq "${expected}" ]]; then
return 0
fi
sleep 1
done
return 1
}
mesh_wait_for_peer_connection() {
local idx="$1"
local target_peer="$2"
local timeout_s="${3:-20}"
local i
for ((i=0; i<timeout_s; i++)); do
local output
output="$(timeout 15s docker run --rm --network "${MESH_NETWORK}" "${MESH_RUNTIME_IMAGE}" mcp-client -url "http://${MESH_PREFIX}-node-${idx}:8080/mcp" -tool "get_mesh_info" 2>/dev/null)"
echo "[$(date +%T)] Node ${idx} get_mesh_info raw output: ${output}"
local connected
connected="$(echo "${output}" | jq -r --arg peer "$target_peer" '.connected_peers | index($peer) != null')"
echo "[$(date +%T)] Node ${idx} connection to ${target_peer}: ${connected}"
if [[ "${connected}" == "true" ]]; then
return 0
fi
sleep 1
done
return 1
}
mesh_wait_for_peer_disconnection() {
local idx="$1"
local target_peer="$2"
local timeout_s="${3:-20}"
local i
for ((i=0; i<timeout_s; i++)); do
local output
output="$(timeout 15s docker run --rm --network "${MESH_NETWORK}" "${MESH_RUNTIME_IMAGE}" mcp-client -url "http://${MESH_PREFIX}-node-${idx}:8080/mcp" -tool "get_mesh_info" 2>/dev/null)"
echo "[$(date +%T)] Node ${idx} get_mesh_info raw output: ${output}"
local connected
connected="$(echo "${output}" | jq -r --arg peer "$target_peer" '.connected_peers | index($peer) != null')"
echo "[$(date +%T)] Node ${idx} connection to ${target_peer}: ${connected}"
if [[ "${connected}" == "false" ]]; then
return 0
fi
sleep 1
done
return 1
}
mesh_get_add_hosts() {
local net="${MESH_NETWORK:-kind}"
# Resolve mock-oidc node IP
local oidc_node
oidc_node=$(kubectl --context="${KUBECONTEXT:-kind-sam-wi-test}" get pod -l app=mock-oidc -o jsonpath='{.items[0].spec.nodeName}')
local oidc_node_ip
oidc_node_ip=$(docker inspect -f "{{(index .NetworkSettings.Networks \"${net}\").IPAddress}}" "${oidc_node}")
# Check if a custom local router container exists in this test scope
local router_ip=""
local custom_router="${MESH_PREFIX}-router"
if docker inspect "${custom_router}" >/dev/null 2>&1; then
router_ip=$(docker inspect -f "{{(index .NetworkSettings.Networks \"${net}\").IPAddress}}" "${custom_router}")
local cp_ip=""
local custom_cp="${MESH_PREFIX}-control-plane"
if docker inspect "${custom_cp}" >/dev/null 2>&1; then
cp_ip=$(docker inspect -f "{{(index .NetworkSettings.Networks \"${net}\").IPAddress}}" "${custom_cp}")
fi
echo "--add-host mock-oidc:${oidc_node_ip} --add-host sam-router:${router_ip} --add-host sam-control-plane:${cp_ip}"
else
# Resolve sam-router-0 node IP
local router_node
router_node=$(kubectl --context="${KUBECONTEXT:-kind-sam-wi-test}" get pod sam-router-0 -o jsonpath='{.spec.nodeName}')
local router_node_ip
router_node_ip=$(docker inspect -f "{{(index .NetworkSettings.Networks \"${net}\").IPAddress}}" "${router_node}")
echo "--add-host mock-oidc:${oidc_node_ip} --add-host sam-router:${router_node_ip} --add-host sam-control-plane:${router_node_ip} --add-host ${router_node}:${router_node_ip}"
fi
}
mesh_setup_suite() {
export PATH="${HOME}/go/bin:$PATH"
mesh_cleanup_stale_resources
if ! command -v docker >/dev/null 2>&1 || ! docker info >/dev/null 2>&1; then
echo "docker not available or daemon not running" >&2
return 1
fi
if ! command -v kind >/dev/null 2>&1; then
echo "kind not available" >&2
return 1
fi
if ! command -v kubectl >/dev/null 2>&1; then
echo "kubectl not available" >&2
return 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "jq not available" >&2
return 1
fi
cd "${BATS_TEST_DIRNAME}/../.."
make
make docker-build
if [[ ! -x "./bin/sam-node" || ! -x "./bin/sam-control-plane" || ! -x "./bin/sam-router" || ! -x "./bin/mcp-client" ]]; then
echo "missing binaries; run: make build" >&2
return 1
fi
export KUBERNETES_CLUSTER_NAME="sam-wi-test"
export KUBECONTEXT="kind-${KUBERNETES_CLUSTER_NAME}"
# Recreating is the default so a local run matches CI, which builds a cluster
# per run. Reuse carries over database PVCs, control plane signing keys and
# bootstrap tokens; E2E_REUSE_CLUSTER=1 trades that fidelity for a faster loop.
local reused_cluster=0
if [[ "${E2E_REUSE_CLUSTER:-0}" == "1" ]] && kind get clusters | grep -q "^${KUBERNETES_CLUSTER_NAME}$"; then
kind export kubeconfig --name "${KUBERNETES_CLUSTER_NAME}"
reused_cluster=1
else
kind delete cluster --name "${KUBERNETES_CLUSTER_NAME}" >/dev/null 2>&1 || true
kind create cluster --name "${KUBERNETES_CLUSTER_NAME}" --config=tests/e2e/fixtures/kind-cluster.yaml
fi
kind load docker-image sam-control-plane:local --name "${KUBERNETES_CLUSTER_NAME}"
kind load docker-image sam-router:local --name "${KUBERNETES_CLUSTER_NAME}"
kind load docker-image sam-node:local --name "${KUBERNETES_CLUSTER_NAME}"
kind load docker-image sam-mock-oidc:local --name "${KUBERNETES_CLUSTER_NAME}"
kubectl --context="${KUBECONTEXT}" apply -f tests/e2e/fixtures/mock-oidc.yaml
mesh_wait_for_rollout deployment/mock-oidc
local kube_issuer
kube_issuer=$(kubectl --context="${KUBECONTEXT}" get --raw /.well-known/openid-configuration | jq -r .issuer)
[[ -n "${kube_issuer}" ]]
kubectl --context="${KUBECONTEXT}" apply -f tests/e2e/fixtures/allow-anonymous-oidc.yaml
export ISSUERS="http://mock-oidc:18080,${kube_issuer}"
local oidc_node
oidc_node=$(kubectl --context="${KUBECONTEXT}" get pod -l app=mock-oidc -o jsonpath='{.items[0].spec.nodeName}')
local oidc_node_ip
oidc_node_ip=$(docker inspect -f "{{(index .NetworkSettings.Networks \"${MESH_NETWORK:-kind}\").IPAddress}}" "${oidc_node}")
local helm_bin="helm"
if ! command -v helm >/dev/null 2>&1; then
if [[ -x "./bin/helm" ]]; then
helm_bin="./bin/helm"
else
echo "helm CLI not found; please install helm or place it in ./bin/helm" >&2
return 1
fi
fi
"${helm_bin}" --kube-context="${KUBECONTEXT}" upgrade --install sam ./charts/sam-mesh \
--namespace default \
--set fullnameOverride="sam" \
--set global.imageTag="local" \
--set controlPlane.oidcIssuer="${ISSUERS//,/\\,}" \
--set controlPlane.allowedAudiences="sam-mesh-audience\,sam-control-plane-audience" \
--set controlPlane.insecureSkipTlsVerify=true \
--set controlPlane.adminToken="super-secret-admin-token" \
--set controlPlane.replicaCount=2 \
--set controlPlane.hostPort=8080 \
--set router.useOidcToken=false \
--set router.hostPort=4501 \
--set console.enabled=false
mesh_wait_for_rollout statefulset/sam-db
mesh_wait_for_rollout deployment/sam-control-plane
mesh_wait_for_job job/sam-bootstrap
# A router surviving a reinstall holds a biscuit no current control plane key
# verifies and a bootstrap token past its 24h default, so it can never lease
# again. Restarting re-enrolls it against the state this run just installed.
if [[ "${reused_cluster}" == "1" ]]; then
kubectl --context="${KUBECONTEXT}" rollout restart statefulset/sam-router
fi
mesh_wait_for_rollout statefulset/sam-router
local i
for ((i=0; i<200; i++)); do
if kubectl --context="${KUBECONTEXT}" logs "sam-router-0" 2>&1 | grep -q "PeerID:"; then
break
fi
sleep 0.1
done
local router_peer_id
router_peer_id=$(kubectl --context="${KUBECONTEXT}" logs "sam-router-0" | grep -oE '12D3Koo[a-zA-Z0-9]+' | head -n 1 || true)
[[ -n "${router_peer_id}" ]]
# The router pod reports Ready before its lease reaches the control plane, and
# a node's /register serves router addresses from that lease, so a node
# started in between enrolls against an empty list and exits.
local router_node_ip
router_node_ip=$(docker inspect -f "{{(index .NetworkSettings.Networks \"${MESH_NETWORK:-kind}\").IPAddress}}" \
"$(kubectl --context="${KUBECONTEXT}" get pod sam-router-0 -o jsonpath='{.spec.nodeName}')")
local lease_deadline=$((SECONDS + 60))
until docker run --rm --network "${MESH_NETWORK:-kind}" python:3.12 \
curl -sf --max-time 5 "http://${router_node_ip}:8080/info" 2>/dev/null | grep -qaF "${router_peer_id}"; do
if ((SECONDS >= lease_deadline)); then
echo "router lease did not reach the control plane within 60s" >&2
return 1
fi
sleep 1
done
echo "${router_peer_id}" > "/tmp/sam-wi-test-router-peer-id"
return 0
}
mesh_teardown_suite() {
cd "${BATS_TEST_DIRNAME}/../.."
mesh_cleanup_stale_resources
# kind delete cluster --name "${KUBERNETES_CLUSTER_NAME:-sam-wi-test}" >/dev/null 2>&1 || true
echo "teardown suite"
}
mesh_start_node() {
local idx="$1"
local flags="${2:-}"
local config_path="${3:-}"
# Extra docker arguments, for tests that need to share something with the
# node container, such as the directory its API socket lives in.
local docker_args="${4:-}"
local name="${MESH_PREFIX}-node-${idx}"
local add_hosts
add_hosts=$(mesh_get_add_hosts)
local router_peer_id
router_peer_id=$(cat "/tmp/${MESH_PREFIX}-router-peer-id")
local mount_args=()
local config_args=()
if [[ -n "${config_path}" ]]; then
local abs_config
abs_config=$(realpath "${config_path}")
mount_args+=(-v "${abs_config}:/etc/sam/node-config.yaml:ro")
config_args+=(--config /etc/sam/node-config.yaml)
fi
docker run -d \
--name "${name}" \
--network "${MESH_NETWORK}" \
--network-alias "${name}" \
${add_hosts} \
"${mount_args[@]}" \
${docker_args} \
-e SAM_CLIENT_SECRET="sam-e2e-secret" \
-e SAM_API_TOKEN="secret-token" \
"${MESH_RUNTIME_IMAGE}" \
/usr/local/bin/sam-node run \
${flags} \
--log-level debug \
--discovery-interval 2s \
--control-plane "http://sam-control-plane:8080" \
--client-id "sam-mesh-audience" \
--oidc-issuer "http://mock-oidc:18080" \
--listen "/ip4/0.0.0.0/udp/5001/quic-v1" \
--listen "/ip4/0.0.0.0/tcp/5002" \
--bind-addr "0.0.0.0:8080" \
--mesh "${MESH_PREFIX}" \
--dht-provider-addr-ttl 5s \
--dht-max-record-age 5s \
"${config_args[@]}" >/dev/null
MESH_CONTAINERS+=("${name}")
}
mesh_start_mock_oidc() {
# No-op: Mock OIDC is running in k8s
return 0
}
mesh_start_router() {
# No-op: router is running in k8s
local peer_id
peer_id=$(cat "/tmp/sam-wi-test-router-peer-id")
echo "${peer_id}" > "/tmp/${MESH_PREFIX}-router-peer-id"
return 0
}
mesh_assert_container_running() {
local name="$1"
if [[ "${name}" == *"-router" ]]; then
kubectl --context="${KUBECONTEXT:-kind-sam-wi-test}" get pod sam-router-0 -o jsonpath='{.status.phase}' | grep -q "Running"
return $?
fi
local state
state="$(docker inspect -f '{{.State.Running}}' "${name}" 2>/dev/null || true)"
[[ "${state}" == "true" ]]
}
fi