Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ __pycache__/
*.out
coverage.*
mobile/sam-node-app/build/
sdk/js/build/
site/public/
site/resources/
rootfs.ext4
Expand Down
71 changes: 63 additions & 8 deletions .github/k8s/sam-probe-cronjob-template.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# The cold path, on a schedule: a node with no identity enrolls with the
# control plane, authenticates to a router, finds a service on the mesh and
# speaks MCP to it through the mesh, then exits. The long-lived canaries only
# do this when a pod restarts, so between rollouts nobody re-checks that a new
# user could still join. Each run enrolls a fresh identity; the control
# plane's --node-retention sweep reclaims the rows.
# speaks MCP to it through the mesh, then reaches the services the SDK
# canaries serve, and exits. The long-lived canaries only do this when a pod
# restarts, so between rollouts nobody re-checks that a new user could still
# join. Each run enrolls a fresh identity; the control plane's
# --node-retention sweep reclaims the rows.
apiVersion: batch/v1
kind: CronJob
metadata:
Expand All @@ -19,8 +20,9 @@ spec:
# One attempt: a retry would hide exactly the flakiness this exists to
# measure. A failed Job is the signal.
backoffLimit: 0
# 180s to be ready plus 120s to reach a provider, with headroom.
activeDeadlineSeconds: 360
# 180s to be ready, 120s to reach a provider, 120s for the SDK
# canaries, with headroom.
activeDeadlineSeconds: 480
template:
metadata:
labels:
Expand Down Expand Up @@ -78,6 +80,10 @@ spec:
# The everything canary's MCP server (sam-node-everything-template.yaml):
# a real server, so the node advertises it and initialize succeeds.
value: everything
- name: SDK_SERVICES
# What the SDK canaries publish (sam-sdk-canary-template.yaml),
# each with a `greet` tool and an A2A endpoint.
value: "greeter-js greeter-py"
resources:
requests:
cpu: 10m
Expand Down Expand Up @@ -157,8 +163,57 @@ spec:
done
REACH_S=$(( $(date +%s) - REACH_START ))

printf '{"probe":"sam-cold-path","ok":true,"ready_s":%d,"router_latency_ms":%s,"connected_peers":%s,"reach_s":%d,"providers_tried":%d,"call_s":%s,"provider":"%s","elapsed_s":%d}\n' \
"$READY_S" "${ROUTER_MS:-null}" "${PEERS:-null}" "$REACH_S" "$TRIED" "$CALL_S" "$PEER" "$(( $(date +%s) - START ))"
# 4. A service an SDK member serves (sam-sdk-canary-template.yaml)
# is reachable from a node the way an agent behind a node
# reaches it: its MCP tool through the node's own MCP API
# (call_remote_tool opens /sam/mcp/1.0.0 to the provider), and
# its A2A endpoint through the egress proxy. The node's MCP
# endpoint is stateful, so one session is opened first.
MCP=http://localhost/mcp
SESSION=""
mcp_post() {
curl -s --unix-socket "$SOCK" -o "$2" -D /tmp/mcp.hdr -X POST "$MCP" \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
${SESSION:+-H "Mcp-Session-Id: $SESSION"} -d "$1"
}
mcp_post '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"sam-probe","version":"0"}}}' /tmp/mcp-init.out \
|| fail sdk "initialize on the node's MCP API failed"
SESSION=$(grep -i '^mcp-session-id:' /tmp/mcp.hdr | tr -d '\r' | cut -d' ' -f2)
[ -n "$SESSION" ] || fail sdk "the node's MCP API returned no session"
mcp_post '{"jsonrpc":"2.0","method":"notifications/initialized"}' /dev/null

SDK_START=$(date +%s)
SDK_PROVIDERS=""
for svc in ${SDK_SERVICES}; do
SDK_PEER=""
LAST_ERR=""
while [ -z "$SDK_PEER" ]; do
if curl -sf --unix-socket "$SOCK" -o /tmp/sdk-providers.json \
"http://localhost/sam/service/discover?type=mcp&name=${svc}&timeout=20s"; then
for p in $(grep -oE '"peer_id":"[^"]+"' /tmp/sdk-providers.json | cut -d'"' -f4); do
mcp_post '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"call_remote_tool","arguments":{"peer_id":"'"$p"'","tool_name":"mcp://'"$svc"'/greet","arguments":{"name":"sam-probe"}}}}' /tmp/sdk-call.out
if grep -q 'hello sam-probe' /tmp/sdk-call.out; then
CODE=$(curl -s --unix-socket "$SOCK" -o /tmp/sdk-card.out -w '%{http_code}' "http://localhost/sam/${p}/a2a/${svc}/card")
if [ "$CODE" = "200" ] && grep -q '"caller"' /tmp/sdk-card.out; then
SDK_PEER=$p
break
fi
LAST_ERR="${p}: a2a card HTTP ${CODE} $(head -c 120 /tmp/sdk-card.out | tr -d '"\n')"
else
LAST_ERR="${p}: $(head -c 160 /tmp/sdk-call.out | tr -d '"\n')"
fi
done
fi
[ -n "$SDK_PEER" ] && break
[ $(( $(date +%s) - SDK_START )) -ge 120 ] && fail sdk "no provider of ${svc} answered greet and its card in 120s (last: ${LAST_ERR:-none discovered})"
sleep 5
done
SDK_PROVIDERS="${SDK_PROVIDERS}${SDK_PROVIDERS:+,}\"${svc}\":\"${SDK_PEER}\""
done
SDK_S=$(( $(date +%s) - SDK_START ))

printf '{"probe":"sam-cold-path","ok":true,"ready_s":%d,"router_latency_ms":%s,"connected_peers":%s,"reach_s":%d,"providers_tried":%d,"call_s":%s,"provider":"%s","sdk_s":%d,"sdk_providers":{%s},"elapsed_s":%d}\n' \
"$READY_S" "${ROUTER_MS:-null}" "${PEERS:-null}" "$REACH_S" "$TRIED" "$CALL_S" "$PEER" "$SDK_S" "$SDK_PROVIDERS" "$(( $(date +%s) - START ))"
volumes:
- name: config-volume
configMap:
Expand Down
124 changes: 124 additions & 0 deletions .github/k8s/sam-sdk-canary-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Two members with no sam-node beside them: the JavaScript and Python SDK
# example servers (sdk/js/examples/serve.ts, sdk/python/examples/serve.py),
# unchanged, each publishing mcp://greeter-<lang> and a2a://greeter-<lang>.
# They enroll with the pod's projected service account token, the way every
# other canary does, and keep their identity in an emptyDir, so a restart
# resumes and a new pod enrolls afresh. What the mesh sees from them is what a
# user of the packages gets.
apiVersion: apps/v1
kind: Deployment
metadata:
name: js-canary-${ENV_NAME}
namespace: sam-canary-${ENV_NAME}
spec:
replicas: 1
selector:
matchLabels:
app: js-canary-${ENV_NAME}
template:
metadata:
labels:
app: js-canary-${ENV_NAME}
sam-canary: "true"
spec:
serviceAccountName: sam-node-sa
containers:
- name: sdk
image: ghcr.io/google/sam-sdk-js:${IMAGE_TAG}
args: ["build/examples/serve.js", "greeter-js"]
env:
- name: SAM_CONTROL_PLANE_URL
value: "http://sam-control-plane-${ENV_NAME}.${NAMESPACE}.svc.cluster.local:8080"
- name: SAM_INSECURE_CONTROL_PLANE
value: "true"
- name: SAM_JWT_PATH
value: /var/run/secrets/tokens/sam-token
- name: SAM_STATE_DIR
value: /var/run/sam/state
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: sam-token
mountPath: /var/run/secrets/tokens
readOnly: true
- name: sam-state
mountPath: /var/run/sam
volumes:
- name: sam-state
emptyDir: {}
- name: sam-token
projected:
sources:
- serviceAccountToken:
path: sam-token
expirationSeconds: 3600
audience: "sam-control-plane-audience"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: python-canary-${ENV_NAME}
namespace: sam-canary-${ENV_NAME}
spec:
replicas: 1
selector:
matchLabels:
app: python-canary-${ENV_NAME}
template:
metadata:
labels:
app: python-canary-${ENV_NAME}
sam-canary: "true"
spec:
serviceAccountName: sam-node-sa
containers:
- name: sdk
image: ghcr.io/google/sam-sdk-python:${IMAGE_TAG}
args: ["examples/serve.py", "greeter-py"]
env:
- name: SAM_CONTROL_PLANE_URL
value: "http://sam-control-plane-${ENV_NAME}.${NAMESPACE}.svc.cluster.local:8080"
- name: SAM_INSECURE_CONTROL_PLANE
value: "true"
- name: SAM_JWT_PATH
value: /var/run/secrets/tokens/sam-token
- name: SAM_STATE_DIR
value: /var/run/sam/state
- name: PYTHONUNBUFFERED
value: "1"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: sam-token
mountPath: /var/run/secrets/tokens
readOnly: true
- name: sam-state
mountPath: /var/run/sam
volumes:
- name: sam-state
emptyDir: {}
- name: sam-token
projected:
sources:
- serviceAccountToken:
path: sam-token
expirationSeconds: 3600
audience: "sam-control-plane-audience"
Loading
Loading