From 2565d52e942516fd36e900a2bfbc0dad23e1d516 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 15:03:59 +0000 Subject: [PATCH 01/18] charts: add sam-node chart for deploying a node with an optional service sidecar --- Makefile | 4 +- charts/sam-node/Chart.yaml | 6 ++ charts/sam-node/README.md | 67 ++++++++++++++ charts/sam-node/templates/_helpers.tpl | 49 ++++++++++ charts/sam-node/templates/configmap.yaml | 9 ++ charts/sam-node/templates/deployment.yaml | 91 +++++++++++++++++++ charts/sam-node/templates/serviceaccount.yaml | 6 ++ charts/sam-node/tests/deployment_test.yaml | 59 ++++++++++++ charts/sam-node/values.yaml | 50 ++++++++++ 9 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 charts/sam-node/Chart.yaml create mode 100644 charts/sam-node/README.md create mode 100644 charts/sam-node/templates/_helpers.tpl create mode 100644 charts/sam-node/templates/configmap.yaml create mode 100644 charts/sam-node/templates/deployment.yaml create mode 100644 charts/sam-node/templates/serviceaccount.yaml create mode 100644 charts/sam-node/tests/deployment_test.yaml create mode 100644 charts/sam-node/values.yaml diff --git a/Makefile b/Makefile index 3a7d45cd..f35eb713 100644 --- a/Makefile +++ b/Makefile @@ -196,7 +196,7 @@ helm-lint: exit 1; \ fi; \ fi; \ - $$HELM_BIN lint ./charts/sam-mesh + $$HELM_BIN lint ./charts/sam-mesh && $$HELM_BIN lint ./charts/sam-node --set controlPlaneUrl=http://required-for-lint:8080 # render the chart to bin/chart/ for inspection; pass extra flags via ARGS, e.g. ARGS="--set gateway.enabled=true" .PHONY: helm-template @@ -211,7 +211,7 @@ lint: fmt helm-lint .PHONY: helm-test helm-test: @helm plugin list 2>/dev/null | grep -q '^unittest' || helm plugin install https://github.com/helm-unittest/helm-unittest - helm unittest charts/sam-mesh + helm unittest charts/sam-mesh charts/sam-node .PHONY: verify verify: diff --git a/charts/sam-node/Chart.yaml b/charts/sam-node/Chart.yaml new file mode 100644 index 00000000..0180780a --- /dev/null +++ b/charts/sam-node/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: sam-node +description: A Helm chart for deploying a SAM node, optionally hosting a service as a sidecar +type: application +version: 0.1.0 +appVersion: "1.0.0" diff --git a/charts/sam-node/README.md b/charts/sam-node/README.md new file mode 100644 index 00000000..13324ac3 --- /dev/null +++ b/charts/sam-node/README.md @@ -0,0 +1,67 @@ +# sam-node + +Deploys one SAM node into a Kubernetes cluster, optionally hosting a service +as a sidecar container. The node authenticates to the control plane with a +projected ServiceAccount token (Workload Identity Federation); the token's +`audience` must be listed in the control plane's `allowedAudiences`. + +## Usage + +A bare node (a caller / mesh participant with no local service): + +```bash +helm install my-node charts/sam-node \ + --set controlPlaneUrl=http://sam-mesh-control-plane:8080 +``` + +A node hosting an MCP service (see `development/examples/*/values.yaml` for +complete, working examples): + +```yaml +controlPlaneUrl: http://sam-mesh-control-plane:8080 +config: + version: v1alpha1 + services: + - type: mcp + name: calculator + description: Simple math operations + target_url: http://127.0.0.1:7777/mcp +service: + name: calc-mcp + image: calc-mcp:local +``` + +Values files stack: keep environment wiring (`controlPlaneUrl`, extra args) +in a base file and the service description in its own, then pass both: +`helm install calc charts/sam-node -f base.yaml -f calc/values.yaml` +(later files win). The kind dev mesh ships such a base at +`development/kind/sam-node.values.yaml`. + +The service container and the node share the pod's network, so `target_url` +points at `127.0.0.1:`. Services declared in `config.services` are +advertised to the mesh via DHT and gossip; `config.attenuation` narrows what +the node's credential permits. The config file is read once at node startup — +the chart rolls the pods on config changes (checksum annotation). + +## Values + +| Key | Default | Meaning | +|-----|---------|---------| +| `controlPlaneUrl` | — (required) | Control plane URL the node enrolls with | +| `audience` | `sam-mesh-audience` | Projected token audience | +| `apiToken` | `devtoken` | Bearer token for the node's local REST API | +| `bindAddr` | `127.0.0.1:8080` | Node API bind address (loopback = pod-private) | +| `extraArgs` | `[]` | Extra sam-node args | +| `config` | empty services | Rendered verbatim as `sam-node.yaml` | +| `service.image` | `""` | Service container image; empty = bare node | +| `service.name/command/env/ports/resources` | — | Service container spec | +| `image.repository/tag/pullPolicy` | `sam-node:local` | Node image | +| `replicaCount` | `1` | Each replica enrolls as its own mesh node | + +## Dynamic registration (alternative to `config.services`) + +A running node also accepts `POST /sam/service/register` on its API +(`bindAddr`, bearer `apiToken`) with a JSON body +`{"service":{"type":"mcp","name":"x","description":"…"},"targetUrl":"http://…"}`. +Registrations are in-memory: after a node restart the registrar must +re-register. Static `config.services` entries need no such care. diff --git a/charts/sam-node/templates/_helpers.tpl b/charts/sam-node/templates/_helpers.tpl new file mode 100644 index 00000000..791aa7fe --- /dev/null +++ b/charts/sam-node/templates/_helpers.tpl @@ -0,0 +1,49 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "sam-node.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name, truncated to the 63-char DNS limit. +*/}} +{{- define "sam-node.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "sam-node.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "sam-node.labels" -}} +helm.sh/chart: {{ include "sam-node.chart" . }} +{{ include "sam-node.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "sam-node.selectorLabels" -}} +app.kubernetes.io/name: {{ include "sam-node.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/charts/sam-node/templates/configmap.yaml b/charts/sam-node/templates/configmap.yaml new file mode 100644 index 00000000..6c7fde64 --- /dev/null +++ b/charts/sam-node/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "sam-node.fullname" . }}-config + labels: + {{- include "sam-node.labels" . | nindent 4 }} +data: + sam-node.yaml: | + {{- .Values.config | toYaml | nindent 4 }} diff --git a/charts/sam-node/templates/deployment.yaml b/charts/sam-node/templates/deployment.yaml new file mode 100644 index 00000000..488d7778 --- /dev/null +++ b/charts/sam-node/templates/deployment.yaml @@ -0,0 +1,91 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "sam-node.fullname" . }} + labels: + {{- include "sam-node.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "sam-node.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "sam-node.selectorLabels" . | nindent 8 }} + annotations: + # A config-only change must roll the pods: the node reads its config + # file once at startup and never reloads it. + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + spec: + serviceAccountName: {{ include "sam-node.fullname" . }} + containers: + - name: sam-node + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + env: + - name: SAM_API_TOKEN + value: {{ .Values.apiToken | quote }} + args: + - "run" + - "--config=/etc/sam/sam-node.yaml" + - "--control-plane={{ required "controlPlaneUrl is required" .Values.controlPlaneUrl }}" + - "--jwt-path=/var/run/secrets/tokens/sam-token" + - "--bind-addr={{ .Values.bindAddr }}" + {{- range .Values.extraArgs }} + - {{ . | quote }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: config + mountPath: /etc/sam + - name: sam-token + mountPath: /var/run/secrets/tokens + readOnly: true + {{- if .Values.service.image }} + - name: {{ .Values.service.name }} + image: {{ .Values.service.image | quote }} + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.service.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.service.env }} + env: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.service.ports }} + ports: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.service.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: config + configMap: + name: {{ include "sam-node.fullname" . }}-config + - name: sam-token + projected: + sources: + - serviceAccountToken: + path: sam-token + expirationSeconds: 3600 + audience: {{ .Values.audience | quote }} diff --git a/charts/sam-node/templates/serviceaccount.yaml b/charts/sam-node/templates/serviceaccount.yaml new file mode 100644 index 00000000..25a24ff8 --- /dev/null +++ b/charts/sam-node/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "sam-node.fullname" . }} + labels: + {{- include "sam-node.labels" . | nindent 4 }} diff --git a/charts/sam-node/tests/deployment_test.yaml b/charts/sam-node/tests/deployment_test.yaml new file mode 100644 index 00000000..e087db27 --- /dev/null +++ b/charts/sam-node/tests/deployment_test.yaml @@ -0,0 +1,59 @@ +suite: sam-node deployment +templates: + # configmap.yaml must be loaded for the checksum/config include to resolve + - templates/deployment.yaml + - templates/configmap.yaml +tests: + - it: fails without controlPlaneUrl + template: templates/deployment.yaml + asserts: + - failedTemplate: {} + + - it: renders a bare node with one container + template: templates/deployment.yaml + set: + controlPlaneUrl: http://sam-mesh-control-plane:8080 + asserts: + - equal: + path: spec.template.spec.containers[0].name + value: sam-node + - lengthEqual: + path: spec.template.spec.containers + count: 1 + - contains: + path: spec.template.spec.containers[0].args + content: "--control-plane=http://sam-mesh-control-plane:8080" + - contains: + path: spec.template.spec.containers[0].args + content: "--bind-addr=127.0.0.1:8080" + - equal: + path: spec.template.spec.volumes[1].projected.sources[0].serviceAccountToken.audience + value: sam-mesh-audience + + - it: renders the service container when service.image is set + template: templates/deployment.yaml + set: + controlPlaneUrl: http://sam-mesh-control-plane:8080 + service: + name: calc-mcp + image: calc-mcp:local + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 2 + - equal: + path: spec.template.spec.containers[1].name + value: calc-mcp + - equal: + path: spec.template.spec.containers[1].image + value: calc-mcp:local + + - it: appends extraArgs + template: templates/deployment.yaml + set: + controlPlaneUrl: http://sam-mesh-control-plane:8080 + extraArgs: ["--discovery-interval=200ms"] + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: "--discovery-interval=200ms" diff --git a/charts/sam-node/values.yaml b/charts/sam-node/values.yaml new file mode 100644 index 00000000..9c66715a --- /dev/null +++ b/charts/sam-node/values.yaml @@ -0,0 +1,50 @@ +image: + repository: sam-node + tag: local + pullPolicy: IfNotPresent + +replicaCount: 1 + +# Required: control plane URL the node enrolls with, +# e.g. http://sam-mesh-control-plane:8080 +controlPlaneUrl: "" + +# Audience of the projected ServiceAccount token; must be one of the control +# plane's allowedAudiences. +audience: sam-mesh-audience + +# Bearer token protecting the node's local REST API. +apiToken: devtoken + +# Address the node API binds inside the pod. Loopback keeps the API +# pod-private; set 0.0.0.0:8080 to expose it on the pod IP. +bindAddr: "127.0.0.1:8080" + +# Extra sam-node args, e.g. ["--discovery-interval=200ms"]. +extraArgs: [] + +# Rendered verbatim as the node's sam-node.yaml: static services the node +# advertises, and local attenuation. +config: + version: v1alpha1 + attenuation: + policies: [] + checks: [] + rules: [] + services: [] + +# Optional service container next to the node; they share localhost, so +# config.services target_url entries point at 127.0.0.1:. +# Rendered only when image is non-empty. +service: + name: service + image: "" + command: [] + env: [] + ports: [] + resources: {} + +resources: {} +nodeSelector: {} +tolerations: [] +affinity: {} From d25ee422933e34468737316dc055c353aab314cf Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 15:14:22 +0000 Subject: [PATCH 02/18] examples: replace sam-node-config.yaml with charts/sam-node values files --- development/examples/calc-mcp/sam-node-config.yaml | 8 -------- development/examples/calc-mcp/values.yaml | 12 ++++++++++++ .../code-reviewer-pool/manager/sam-node-config.yaml | 8 -------- .../examples/code-reviewer-pool/manager/values.yaml | 12 ++++++++++++ .../code-reviewer-pool/reviewer/sam-node-config.yaml | 8 -------- .../examples/code-reviewer-pool/reviewer/values.yaml | 12 ++++++++++++ .../examples/everything-mcp/sam-node-config.yaml | 8 -------- development/examples/everything-mcp/values.yaml | 12 ++++++++++++ .../examples/gemini-buddy-mcp/sam-node-config.yaml | 8 -------- development/examples/gemini-buddy-mcp/values.yaml | 12 ++++++++++++ .../examples/greeter-mcp/sam-node-config.yaml | 8 -------- development/examples/greeter-mcp/values.yaml | 12 ++++++++++++ development/kind/sam-node.values.yaml | 4 ++++ 13 files changed, 76 insertions(+), 48 deletions(-) delete mode 100644 development/examples/calc-mcp/sam-node-config.yaml create mode 100644 development/examples/calc-mcp/values.yaml delete mode 100644 development/examples/code-reviewer-pool/manager/sam-node-config.yaml create mode 100644 development/examples/code-reviewer-pool/manager/values.yaml delete mode 100644 development/examples/code-reviewer-pool/reviewer/sam-node-config.yaml create mode 100644 development/examples/code-reviewer-pool/reviewer/values.yaml delete mode 100644 development/examples/everything-mcp/sam-node-config.yaml create mode 100644 development/examples/everything-mcp/values.yaml delete mode 100644 development/examples/gemini-buddy-mcp/sam-node-config.yaml create mode 100644 development/examples/gemini-buddy-mcp/values.yaml delete mode 100644 development/examples/greeter-mcp/sam-node-config.yaml create mode 100644 development/examples/greeter-mcp/values.yaml create mode 100644 development/kind/sam-node.values.yaml diff --git a/development/examples/calc-mcp/sam-node-config.yaml b/development/examples/calc-mcp/sam-node-config.yaml deleted file mode 100644 index 1abd60e8..00000000 --- a/development/examples/calc-mcp/sam-node-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: "v1alpha1" -attenuation: - policies: [] -services: - - type: "mcp" - name: "calculator" - description: "Simple math operations" - target_url: "http://127.0.0.1:7777/mcp" diff --git a/development/examples/calc-mcp/values.yaml b/development/examples/calc-mcp/values.yaml new file mode 100644 index 00000000..edc058a3 --- /dev/null +++ b/development/examples/calc-mcp/values.yaml @@ -0,0 +1,12 @@ +config: + version: v1alpha1 + attenuation: + policies: [] + services: + - type: mcp + name: calculator + description: Simple math operations + target_url: http://127.0.0.1:7777/mcp +service: + name: calc-mcp + image: calc-mcp:local diff --git a/development/examples/code-reviewer-pool/manager/sam-node-config.yaml b/development/examples/code-reviewer-pool/manager/sam-node-config.yaml deleted file mode 100644 index 18991131..00000000 --- a/development/examples/code-reviewer-pool/manager/sam-node-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: "v1alpha1" -attenuation: - policies: [] -services: - - type: "mcp" - name: "pool-manager" - description: "Leases free workers from the code-reviewer pool (acquire/release)" - target_url: "http://127.0.0.1:7780/mcp" diff --git a/development/examples/code-reviewer-pool/manager/values.yaml b/development/examples/code-reviewer-pool/manager/values.yaml new file mode 100644 index 00000000..dfa3e5cc --- /dev/null +++ b/development/examples/code-reviewer-pool/manager/values.yaml @@ -0,0 +1,12 @@ +config: + version: v1alpha1 + attenuation: + policies: [] + services: + - type: mcp + name: pool-manager + description: Leases free workers from the code-reviewer pool (acquire/release) + target_url: http://127.0.0.1:7780/mcp +service: + name: pool-manager + image: pool-manager:local diff --git a/development/examples/code-reviewer-pool/reviewer/sam-node-config.yaml b/development/examples/code-reviewer-pool/reviewer/sam-node-config.yaml deleted file mode 100644 index bad855af..00000000 --- a/development/examples/code-reviewer-pool/reviewer/sam-node-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: "v1alpha1" -attenuation: - policies: [] -services: - - type: "mcp" - name: "code-reviewer" - description: "Reviews code snippets via a Gemini-backed agent" - target_url: "http://127.0.0.1:7779/mcp" diff --git a/development/examples/code-reviewer-pool/reviewer/values.yaml b/development/examples/code-reviewer-pool/reviewer/values.yaml new file mode 100644 index 00000000..2cd2f0ea --- /dev/null +++ b/development/examples/code-reviewer-pool/reviewer/values.yaml @@ -0,0 +1,12 @@ +config: + version: v1alpha1 + attenuation: + policies: [] + services: + - type: mcp + name: code-reviewer + description: Reviews code snippets via a Gemini-backed agent + target_url: http://127.0.0.1:7779/mcp +service: + name: reviewer + image: reviewer:local diff --git a/development/examples/everything-mcp/sam-node-config.yaml b/development/examples/everything-mcp/sam-node-config.yaml deleted file mode 100644 index 3107e5df..00000000 --- a/development/examples/everything-mcp/sam-node-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: "v1alpha1" -attenuation: - policies: [] -services: - - type: "mcp" - name: "everything" - description: "MCP everything test server (tools, resources, prompts)" - target_url: "http://127.0.0.1:3001/mcp" diff --git a/development/examples/everything-mcp/values.yaml b/development/examples/everything-mcp/values.yaml new file mode 100644 index 00000000..ee2aa8f4 --- /dev/null +++ b/development/examples/everything-mcp/values.yaml @@ -0,0 +1,12 @@ +config: + version: v1alpha1 + attenuation: + policies: [] + services: + - type: mcp + name: everything + description: MCP everything test server (tools, resources, prompts) + target_url: http://127.0.0.1:3001/mcp +service: + name: everything-mcp + image: everything-mcp:local diff --git a/development/examples/gemini-buddy-mcp/sam-node-config.yaml b/development/examples/gemini-buddy-mcp/sam-node-config.yaml deleted file mode 100644 index ff1b533b..00000000 --- a/development/examples/gemini-buddy-mcp/sam-node-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: "v1alpha1" -attenuation: - policies: [] -services: - - type: "mcp" - name: "gemini-buddy" - description: "A friendly, funny Gemini-backed buddy you can chat with over multiple turns" - target_url: "http://127.0.0.1:7780/mcp" diff --git a/development/examples/gemini-buddy-mcp/values.yaml b/development/examples/gemini-buddy-mcp/values.yaml new file mode 100644 index 00000000..2dd762e6 --- /dev/null +++ b/development/examples/gemini-buddy-mcp/values.yaml @@ -0,0 +1,12 @@ +config: + version: v1alpha1 + attenuation: + policies: [] + services: + - type: mcp + name: gemini-buddy + description: A friendly, funny Gemini-backed buddy you can chat with over multiple turns + target_url: http://127.0.0.1:7780/mcp +service: + name: gemini-buddy-mcp + image: gemini-buddy-mcp:local diff --git a/development/examples/greeter-mcp/sam-node-config.yaml b/development/examples/greeter-mcp/sam-node-config.yaml deleted file mode 100644 index 12722585..00000000 --- a/development/examples/greeter-mcp/sam-node-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: "v1alpha1" -attenuation: - policies: [] -services: - - type: "mcp" - name: "greeter" - description: "Friendly greetings and shouts" - target_url: "http://127.0.0.1:7778/mcp" diff --git a/development/examples/greeter-mcp/values.yaml b/development/examples/greeter-mcp/values.yaml new file mode 100644 index 00000000..01931915 --- /dev/null +++ b/development/examples/greeter-mcp/values.yaml @@ -0,0 +1,12 @@ +config: + version: v1alpha1 + attenuation: + policies: [] + services: + - type: mcp + name: greeter + description: Friendly greetings and shouts + target_url: http://127.0.0.1:7778/mcp +service: + name: greeter-mcp + image: greeter-mcp:local diff --git a/development/kind/sam-node.values.yaml b/development/kind/sam-node.values.yaml new file mode 100644 index 00000000..9c421fcc --- /dev/null +++ b/development/kind/sam-node.values.yaml @@ -0,0 +1,4 @@ +# Kind-mesh wiring shared by every sam-node release; stack an example's +# values on top: helm install charts/sam-node -f this-file -f /values.yaml +controlPlaneUrl: http://sam-mesh-control-plane:8080 +extraArgs: ["--discovery-interval=200ms"] From fe43d8de1a71a8d033b1f3b2069199e483cc7ab1 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 15:19:53 +0000 Subject: [PATCH 03/18] kind: deploy sam infrastructure only, drop the mesh-config node templating --- development/kind/kind-config.yaml | 5 -- development/kind/mesh-config.e2e.yaml | 4 -- development/kind/mesh-config.yaml | 8 --- development/kind/node.template.yaml | 55 -------------------- development/kind/run-local-node.sh | 2 +- development/kind/run.sh | 74 +++++---------------------- 6 files changed, 15 insertions(+), 133 deletions(-) delete mode 100644 development/kind/mesh-config.e2e.yaml delete mode 100644 development/kind/mesh-config.yaml delete mode 100644 development/kind/node.template.yaml diff --git a/development/kind/kind-config.yaml b/development/kind/kind-config.yaml index 80906475..070caa0d 100644 --- a/development/kind/kind-config.yaml +++ b/development/kind/kind-config.yaml @@ -6,8 +6,3 @@ nodes: labels: sam-role: control-plane - role: worker - labels: - sam-role: node-a - - role: worker - labels: - sam-role: node-b diff --git a/development/kind/mesh-config.e2e.yaml b/development/kind/mesh-config.e2e.yaml deleted file mode 100644 index 86753d6c..00000000 --- a/development/kind/mesh-config.e2e.yaml +++ /dev/null @@ -1,4 +0,0 @@ -# E2E mesh layout: pins calc-mcp so the kind-mesh-e2e check can discover -# mcp://calculator/add. Select it with MESH_CONFIG=development/kind/mesh-config.e2e.yaml. -node-a: -node-b: calc-mcp diff --git a/development/kind/mesh-config.yaml b/development/kind/mesh-config.yaml deleted file mode 100644 index 58430ebb..00000000 --- a/development/kind/mesh-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# node -> service. A blank value means a bare node (no service, e.g. a caller). -# Set a value to host a service on that node; the value is a folder path under -# development/examples/, e.g: -# node-a: calc-mcp -# node-b: code-reviewer-pool/reviewer -# All nodes ship bare by default — assign services to suit what you're testing. -node-a: -node-b: diff --git a/development/kind/node.template.yaml b/development/kind/node.template.yaml deleted file mode 100644 index c7e8b445..00000000 --- a/development/kind/node.template.yaml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: ${NODE}-sa - namespace: ${NAMESPACE} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: ${NODE} - namespace: ${NAMESPACE} -spec: - replicas: 1 - selector: - matchLabels: - app: ${NODE} - template: - metadata: - labels: - app: ${NODE} - spec: - nodeSelector: - sam-role: ${NODE} - serviceAccountName: ${NODE}-sa - containers: - - name: sam-node - image: sam-node:${IMAGE_TAG} - imagePullPolicy: IfNotPresent - env: - - name: SAM_API_TOKEN - value: "devtoken" - args: - - "run" - - "--control-plane=${CONTROL_PLANE_URL}" - - "--jwt-path=/var/run/secrets/tokens/sam-token" - - "--bind-addr=0.0.0.0:8080" - - "--discovery-interval=200ms" -${CONFIG_ARG} - ports: - - { containerPort: 8080, name: mcp, protocol: TCP } - volumeMounts: - - name: sam-token - mountPath: /var/run/secrets/tokens - readOnly: true -${CONFIG_MOUNT} -${SIDECAR} - volumes: - - name: sam-token - projected: - sources: - - serviceAccountToken: - path: sam-token - expirationSeconds: 3600 - audience: sam-mesh-audience -${CONFIG_VOLUME} diff --git a/development/kind/run-local-node.sh b/development/kind/run-local-node.sh index 469d746b..5814ffae 100755 --- a/development/kind/run-local-node.sh +++ b/development/kind/run-local-node.sh @@ -2,7 +2,7 @@ # Enroll a locally-built ./bin/sam-node into the kind mesh the way a real external node joins: # a bootstrap token over the control plane's gateway address, peer traffic through the router's # node-IP multiaddrs. Extra args pass through, e.g. to host a service: -# ARGS="--config development/examples/calc-mcp/sam-node-config.yaml" +# ARGS="--config my-node.yaml" set -euo pipefail CLUSTER="sam-kind" diff --git a/development/kind/run.sh b/development/kind/run.sh index 96dc8df3..424ec42f 100755 --- a/development/kind/run.sh +++ b/development/kind/run.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash -# Kind dev mesh: a control plane and router plus the nodes from mesh-config.yaml, each pinned to its -# own k8s node, with live per-pod logs in named tmux panes. Control plane, console and Dex are reached -# over Gateway API LoadBalancer addresses from cloud-provider-kind (started here); the router at its node IP. +# Kind dev mesh: control plane, router, console and Dex, with live per-pod logs in named tmux +# panes. No sam-nodes are deployed — put services on the mesh with charts/sam-node (see the +# epilogue below) or enroll a local node. Gateway addresses come from cloud-provider-kind. set -euo pipefail CLUSTER="sam-kind" @@ -24,7 +24,7 @@ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" cd "${PROJECT_ROOT}" check_prereqs() { - local bins=(kind kubectl docker jq envsubst awk) + local bins=(kind kubectl docker jq envsubst) [[ "${1:-}" != "-s" ]] && bins+=(tmux) for bin in "${bins[@]}"; do command -v "$bin" >/dev/null 2>&1 || { echo "missing prerequisite: $bin" >&2; exit 1; } @@ -122,30 +122,6 @@ apply_dex() { | kubectl --context "${KCTX}" -n "${NAMESPACE}" apply -f - } -render_and_apply() { - local node="$1" svc="$2" - local CONFIG_ARG="" CONFIG_MOUNT="" SIDECAR="" CONFIG_VOLUME="" - if [[ -n "$svc" ]]; then - local dir="${PROJECT_ROOT}/development/examples/${svc}" - local name="$(basename "$svc")" - [[ -d "$dir" ]] || { echo "service '${svc}' (node ${node}) not found in development/examples/" >&2; exit 1; } - echo "Building service image ${name}:${IMAGE_TAG}…" - docker build -t "${name}:${IMAGE_TAG}" "$dir" - kind load docker-image --name "${CLUSTER}" "${name}:${IMAGE_TAG}" - kubectl --context "${KCTX}" -n "${NAMESPACE}" create configmap "${node}-config" \ - --from-file=sam-node.yaml="${dir}/sam-node-config.yaml" \ - --dry-run=client -o yaml | kubectl --context "${KCTX}" apply -f - - CONFIG_ARG=' - "--config=/etc/sam/sam-node.yaml"' - CONFIG_MOUNT=$' - name: config\n mountPath: /etc/sam' - SIDECAR=$' - name: '"${name}"$'\n image: '"${name}:${IMAGE_TAG}"$'\n imagePullPolicy: IfNotPresent' - CONFIG_VOLUME=$' - name: config\n configMap:\n name: '"${node}-config" - fi - NODE="$node" CONTROL_PLANE_URL="$CONTROL_PLANE_URL" CONFIG_ARG="$CONFIG_ARG" CONFIG_MOUNT="$CONFIG_MOUNT" SIDECAR="$SIDECAR" CONFIG_VOLUME="$CONFIG_VOLUME" \ - envsubst '${NODE} ${NAMESPACE} ${CONTROL_PLANE_URL} ${IMAGE_TAG} ${CONFIG_ARG} ${CONFIG_MOUNT} ${SIDECAR} ${CONFIG_VOLUME}' \ - < "${SCRIPT_DIR}/node.template.yaml" | kubectl --context "${KCTX}" apply -f - - -} - # logs: $1 = pane name (printed in-pane and set as the pane title); $2 = logs target. logs() { echo "printf '\\033[1;36m==== %s ====\\033[0m\\n' '$1'; kubectl --context ${KCTX} -n ${NAMESPACE} logs -f $2; echo; echo '[$1 pane exited; press enter]'; read"; } @@ -157,15 +133,11 @@ show_cluster_logs() { tmuxs new-session -d -s "${SESSION}" -n mesh "$(logs control-plane 'deploy/sam-mesh-control-plane')" \; set -t "${SESSION}" destroy-unattached off tmuxs split-window -t "${SESSION}:0" "$(logs router 'statefulset/sam-mesh-router')" - for node in "${NODES[@]}"; do - tmuxs split-window -t "${SESSION}:0" "$(logs "$node" "deploy/${node} -c sam-node")" - tmuxs select-layout -t "${SESSION}:0" tiled - done tmuxs set-option -t "${SESSION}" -g pane-border-status top tmuxs set-option -t "${SESSION}" -g pane-border-format ' #{pane_title} ' - # Title the tmux panes in creation order: control-plane, router, then the nodes. - titles=(control-plane router "${NODES[@]}") + # Title the tmux panes in creation order: control-plane, router. + titles=(control-plane router) i=0 for pane in $(tmuxs list-panes -t "${SESSION}:0" -F '#{pane_id}'); do tmuxs select-pane -t "$pane" -T "${titles[$i]}" @@ -176,15 +148,6 @@ show_cluster_logs() { tmuxs attach-session -t "${SESSION}" } -# Read the node -> service assignment into NODE_LINES (each line: -# " ") and the NODES array. Defaults to mesh-config.yaml; -# override with MESH_CONFIG (e.g. the e2e lane pins calc-mcp via mesh-config.e2e.yaml). -read_mesh_nodes() { - mapfile -t NODE_LINES < <(awk -F: '/^[A-Za-z0-9_-]+:/{n=$1; s=$2; gsub(/[[:space:]]/,"",n); gsub(/[[:space:]]/,"",s); print n, s}' "${MESH_CONFIG:-${SCRIPT_DIR}/mesh-config.yaml}") - NODES=() - for line in "${NODE_LINES[@]}"; do NODES+=("${line%% *}"); done -} - ### MAIN ### @@ -195,7 +158,6 @@ if [[ $# -gt 0 && "$1" != "-s" && "$1" != "-l" && "$1" != "-d" ]]; then fi if [[ "${1:-}" == "-l" ]]; then - read_mesh_nodes show_cluster_logs exit 0 fi @@ -218,8 +180,6 @@ make docker-build-control-plane docker-build-router docker-build-node docker-bui echo "== Loading sam images into kind ==" kind load docker-image --name "${CLUSTER}" "sam-control-plane:${IMAGE_TAG}" "sam-router:${IMAGE_TAG}" "sam-node:${IMAGE_TAG}" "sam-console:${IMAGE_TAG}" -read_mesh_nodes - # Apply the control plane and router, and wait until they accept connections ISSUER="$(kubectl --context "${KCTX}" get --raw /.well-known/openid-configuration | jq -r .issuer)" [[ -n "$ISSUER" ]] || { echo "could not determine cluster OIDC issuer" >&2; exit 1; } @@ -232,8 +192,8 @@ CONTROL_PLANE_ISSUERS="${ISSUER}" # match Dex's static client. ALLOWED_AUDIENCES="sam-console,sam-mesh-audience,sam-control-plane-audience" -# Only the node template's envsubst reads these from the environment. -export NAMESPACE IMAGE_TAG +# 00-namespace-rbac.yaml's envsubst reads this from the environment. +export NAMESPACE echo "== Applying namespace and RBAC cluster rules ==" envsubst '${NAMESPACE}' < "${SCRIPT_DIR}/00-namespace-rbac.yaml" | kubectl --context "${KCTX}" apply -f - @@ -300,24 +260,18 @@ echo "== Restarting the console with the final issuer ==" kubectl --context "${KCTX}" -n "${NAMESPACE}" rollout restart deployment/sam-mesh-console kubectl --context "${KCTX}" -n "${NAMESPACE}" rollout status deployment/sam-mesh-console --timeout=180s - -echo "== Applying sam-nodes ==" -for line in "${NODE_LINES[@]}"; do - node="${line%% *}"; svc="${line#* }"; [[ "$svc" == "$node" ]] && svc="" - render_and_apply "$node" "$svc" -done - -echo "== Waiting for sam-nodes ==" -for node in "${NODES[@]}"; do - kubectl --context "${KCTX}" -n "${NAMESPACE}" wait --for=condition=available --timeout=180s "deployment/${node}" -done - echo echo "Mesh up." echo " console: ${CONSOLE_URL}" echo " control plane: http://${MAIN_IP}" echo " dex: ${OIDC_ISSUER}" echo +echo "To put a service on the mesh, deploy an example with charts/sam-node:" +echo " docker build -t calc-mcp:local development/examples/calc-mcp" +echo " kind load docker-image --name ${CLUSTER} calc-mcp:local" +echo " ${HELM} --kube-context ${KCTX} -n ${NAMESPACE} install calc-mcp charts/sam-node \\" +echo " -f development/kind/sam-node.values.yaml -f development/examples/calc-mcp/values.yaml" +echo echo "To drive the mesh, enroll a local node in another shell (it stays in the foreground):" echo " make build && make kind-local-node" echo "then call its MCP API on 127.0.0.1:9099:" From de88f4a4b48d0ac0f294606fb03f8e2d0a1b72e0 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 15:23:57 +0000 Subject: [PATCH 04/18] e2e: deploy calc-mcp through charts/sam-node --- .github/workflows/kind-mesh-e2e.yml | 2 +- development/kind/test-mesh-e2e.sh | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/kind-mesh-e2e.yml b/.github/workflows/kind-mesh-e2e.yml index 87ecbfdb..2bee0faa 100644 --- a/.github/workflows/kind-mesh-e2e.yml +++ b/.github/workflows/kind-mesh-e2e.yml @@ -61,7 +61,7 @@ jobs: run: make build - name: Bring up the kind mesh - run: MESH_CONFIG=development/kind/mesh-config.e2e.yaml make kind-up ARGS="-s" + run: make kind-up ARGS="-s" - name: Run e2e mesh tests run: make kind-e2e-mesh diff --git a/development/kind/test-mesh-e2e.sh b/development/kind/test-mesh-e2e.sh index 7bda43b3..83568485 100755 --- a/development/kind/test-mesh-e2e.sh +++ b/development/kind/test-mesh-e2e.sh @@ -15,6 +15,21 @@ set -euo pipefail +KCTX="kind-sam-kind" +NAMESPACE="sam-kind" + +# Same helm fallback as run.sh: CI and dev boxes may only have ./bin/helm. +HELM="helm" +command -v helm >/dev/null 2>&1 || HELM="./bin/helm" + +echo "== Deploying calc-mcp via charts/sam-node ==" +docker build -t calc-mcp:local development/examples/calc-mcp +kind load docker-image --name sam-kind calc-mcp:local +"${HELM}" --kube-context "${KCTX}" -n "${NAMESPACE}" install calc-mcp charts/sam-node \ + -f development/kind/sam-node.values.yaml \ + -f development/examples/calc-mcp/values.yaml +kubectl --context "${KCTX}" -n "${NAMESPACE}" rollout status deployment/calc-mcp-sam-node --timeout=180s + # If running locally, we might want to store logs in a temp dir LOG_DIR="${RUNNER_TEMP:-$(mktemp -d)}" From 087eef99a52cfbb9079c7030686c1fa03346717b Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 15:31:09 +0000 Subject: [PATCH 05/18] docs: kind mesh deploys infra only; services ship via charts/sam-node --- site/content/docs/development/_index.md | 4 +- .../docs/development/kubernetes-deployment.md | 134 +++++++++--------- site/content/docs/use-cases/gemini-buddy.md | 19 +-- .../content/docs/use-cases/warm-agent-pool.md | 54 ++++--- .../docs/user/kubernetes-deployment.md | 6 + 5 files changed, 107 insertions(+), 110 deletions(-) diff --git a/site/content/docs/development/_index.md b/site/content/docs/development/_index.md index 1421f478..8a5c35f0 100644 --- a/site/content/docs/development/_index.md +++ b/site/content/docs/development/_index.md @@ -88,10 +88,10 @@ make test-e2e-container For end-to-end integration testing in a local Kubernetes environment, the repository provides `make` targets that stand up a complete mesh in `kind` with a single command: ```bash -make kind-up # create the sam-kind cluster, build+load images, deploy control-plane + router + nodes +make kind-up # create the sam-kind cluster, build+load images, deploy the sam infrastructure make kind-local-node # enroll a locally-built ./bin/sam-node into the mesh make kind-e2e-mesh # run the end-to-end discover-and-call check make kind-down # tear the cluster down ``` -`make kind-up` builds the `sam-control-plane:local`, `sam-router:local`, and `sam-node:local` images, creates a `sam-kind` cluster, and deploys the control-plane and router plus the nodes declared in `development/kind/mesh-config.yaml`. See the [Kubernetes Deployment and Local Testing Guide](kubernetes-deployment/#1-local-testing-with-kind) for details. +`make kind-up` builds the `sam-control-plane:local`, `sam-router:local`, `sam-node:local` and `sam-console:local` images, creates a `sam-kind` cluster, and deploys the control plane, router, console and Dex — no sam-nodes. Deploy services with `charts/sam-node`; see the [Kubernetes Deployment and Local Testing Guide](kubernetes-deployment/#1-local-testing-with-kind) for details. diff --git a/site/content/docs/development/kubernetes-deployment.md b/site/content/docs/development/kubernetes-deployment.md index 9f36f6ac..e6077e3b 100644 --- a/site/content/docs/development/kubernetes-deployment.md +++ b/site/content/docs/development/kubernetes-deployment.md @@ -11,7 +11,7 @@ This guide explains how to deploy the SAM control plane and router in a Kubernet ## 1. Local Testing with Kind -The repository ships a one-command local mesh under `development/kind/`, driven by `make` targets. This is the fastest way to get a running control plane, router and a few nodes on your machine. +The repository ships a one-command local mesh under `development/kind/`, driven by `make` targets. This is the fastest way to get a running control plane, router and console on your machine — ready for you to deploy services onto. ### Automated Mesh (Recommended) @@ -19,10 +19,17 @@ The repository ships a one-command local mesh under `development/kind/`, driven make kind-up ``` -This creates a `sam-kind` cluster (one control-plane plus three workers — one labeled `sam-role: control-plane`, and one each for `node-a` and `node-b`), builds the `sam-control-plane:local`, `sam-router:local`, and `sam-node:local` images, loads them into the cluster, and deploys: +This creates a `sam-kind` cluster (one control-plane plus two workers — one +labeled `sam-role: control-plane` for the router), builds the +`sam-control-plane:local`, `sam-router:local`, `sam-node:local` and +`sam-console:local` images, loads them into the cluster, and deploys: - The **control plane**, configured to trust the cluster's own OIDC issuer. -- Two **nodes** declared in `development/kind/mesh-config.yaml` (`node-a` and `node-b`), both **bare** by default — assign services to suit what you're testing. +- The **console**. +- **Dex**. +- The **router**. +- **No sam-nodes.** The mesh comes up empty; put services on it with the + `charts/sam-node` chart (next section) or enroll a local node. In-cluster nodes authenticate to the control plane via **Workload Identity Federation** (projected ServiceAccount tokens), so no static secrets or mock OIDC provider are needed. @@ -33,7 +40,7 @@ The mesh is exposed through Gateway API LoadBalancer addresses, so there are no - **Dex** on its own address, deployed from `development/kind/dex.yaml` rather than the chart — Dex is an independent component the chart no longer bundles. - The **router** at its own node's IP on port 4501, TCP **and** QUIC, announced from `status.hostIP`. -Once everything is up, `make kind-up` opens a tmux session with live per-pod logs (control plane, router and each node in its own pane). Manage the mesh with: +Once everything is up, `make kind-up` opens a tmux session with live per-pod logs (control plane and router, each in its own pane). Manage the mesh with: ```bash make kind-up ARGS=-s # bring the mesh up without attaching the log view @@ -41,65 +48,62 @@ make kind-logs # (re)attach the live-logs tmux session make kind-down # delete the sam-kind cluster and stop cloud-provider-kind ``` -### Mesh Layout (`mesh-config.yaml`) +### Deploying a Service -The nodes that make up the dev mesh are declared in `development/kind/mesh-config.yaml`. Each entry maps a node to an optional service: +A service is any backend a node advertises to the mesh (`type: mcp` or +`type: inference`). In Kubernetes a service ships as a **`charts/sam-node` +release**: one pod holding your service container and a `sam-node` sidecar +that advertises it. The repository ships ready-made examples under +`development/examples/`; each is a `Dockerfile` plus a `values.yaml` +describing only the service — kind-wide wiring (control plane URL, fast +discovery) lives once in `development/kind/sam-node.values.yaml` and is +stacked underneath with a second `-f`. + +Deploy one (calc-mcp) into the running kind mesh: + +```bash +docker build -t calc-mcp:local development/examples/calc-mcp +kind load docker-image --name sam-kind calc-mcp:local +helm --kube-context kind-sam-kind -n sam-kind install calc-mcp charts/sam-node \ + -f development/kind/sam-node.values.yaml \ + -f development/examples/calc-mcp/values.yaml +``` + +To write your own service, copy an example folder: a backend listening on a +local port, a `Dockerfile`, and a `values.yaml` declaring the service — ```yaml -# node -> service. A blank value means a bare node (no service, e.g. a caller). -# Set a value to host a service on that node; the value is a folder path under -# development/examples/, e.g: -# node-a: calc-mcp -# node-b: code-reviewer-pool/reviewer -# All nodes ship bare by default — assign services to suit what you're testing. -node-a: -node-b: +config: + version: v1alpha1 + attenuation: + policies: [] + services: + - type: mcp + name: my-service + description: What it does + target_url: http://127.0.0.1:7779/mcp +service: + name: my-mcp + image: my-mcp:local ``` -- The key is the node's name. The cluster currently ships with a control plane and router plus these **two** agent nodes, both bare by default; each is pinned to a matching worker via the `sam-role` labels in `kind-config.yaml`. -- A **blank** value is a bare node — a `sam-node` with no local service, useful as a caller/consumer. -- A **non-blank** value is a folder name under `development/examples/`. That service is built and deployed as a **sidecar** next to the node, and the node is configured to advertise it to the mesh. - -When a node has a service, `make kind-up` builds the service image from its `Dockerfile`, loads it into the cluster, and mounts the service's `sam-node-config.yaml` into the node. Because `make kind-up` only runs against a fresh cluster (it refuses if `sam-kind` already exists), **services are (re)deployed on cluster recreation** — after editing `mesh-config.yaml` or a service, run `make kind-down && make kind-up` to pick up the change. - -### Adding and Testing a New Service - -A service is any backend a node advertises to the mesh. Its kind is set by the `type` field in `sam-node-config.yaml`. SAM currently supports `mcp` (an MCP server) and `inference` (an LLM inference endpoint). The repository ships example MCP services under `development/examples/` (`calc-mcp`, `greeter-mcp`, `code-reviewer-pool/reviewer`, and `everything-mcp`) which are the easiest starting point. Using `calc-mcp` as a template: - -1. **Create the service folder** `development/examples/my-mcp/` with: - - The service backend (e.g. `my_server.py`) listening on a local port, plus a `Dockerfile` and any `requirements.txt`. - - A `sam-node-config.yaml` declaring the service. Set `type` to the service kind and point `target_url` at the backend's local port: - ```yaml - version: "v1alpha1" - attenuation: - policies: - services: - - type: "mcp" - name: "my-service" - description: "What it does" - target_url: "http://127.0.0.1:7779/mcp" - ``` - The sidecar and `sam-node` share the pod's network, so `target_url` is always `127.0.0.1:`, where `` matches the port your service listens on. - -2. **Assign it to a node** in `mesh-config.yaml` — set the value on a free node slot (`node-a` or `node-b`): - ```yaml - node-a: my-mcp - ``` - > [!NOTE] - > There are two node slots because `kind-config.yaml` defines two workers labeled `sam-role: node-a|node-b`. To host more than two services at once, add a matching labeled worker there too. +The service container and `sam-node` share the pod's network, so `target_url` +is always `127.0.0.1:`. Iterate with `docker build … && kind load … && +helm upgrade calc-mcp charts/sam-node -f … -f …` — the chart rolls the pods +on config changes. Remove a service with `helm uninstall`. -3. **Recreate the cluster** so the new service is built and deployed: - ```bash - make kind-down && make kind-up - ``` +Discover and call it from another node — enroll a local node and use the MCP +client: -4. **Discover and call it** from another node — enroll a local node and use the MCP client: - ```bash - make kind-local-node - # in another shell: - ./bin/mcp-client -url http://127.0.0.1:9099/mcp -token devtoken -tool find_remote_tools -args '{}' - ``` - `find_remote_tools` lists the discovered tools (e.g. `mcp://my-service/...`) and the peer hosting them; pass that `peer_id` and `tool_name` to `call_remote_tool` to invoke it. +```bash +make kind-local-node +# in another shell: +./bin/mcp-client -url http://127.0.0.1:9099/mcp -token devtoken -tool find_remote_tools -args '{}' +``` + +`find_remote_tools` lists the discovered tools (e.g. `mcp://my-service/...`) +and the peer hosting them; pass that `peer_id` and `tool_name` to +`call_remote_tool` to invoke it. ### Enrolling a Local Node @@ -110,11 +114,9 @@ make build # produce ./bin/sam-node make kind-local-node ``` -This mints a bootstrap token through the control plane's `/admin` API and runs `./bin/sam-node` against the control plane's gateway address — the same credential and path a real external node uses — exposing its MCP API on `127.0.0.1:9099` with the API token `devtoken`. Extra flags pass through via `ARGS`, e.g. to host an example service: - -```bash -make kind-local-node ARGS="--config development/examples/calc-mcp/sam-node-config.yaml" -``` +This mints a bootstrap token through the control plane's `/admin` API and runs `./bin/sam-node` against the control plane's gateway address — the same credential and path a real external node uses — exposing its MCP API on `127.0.0.1:9099` with the API token `devtoken`. Extra flags pass through via `ARGS`, e.g. `make kind-local-node +ARGS="--config my-node.yaml"` to host a service from a local config file +(same schema as the `config:` block in a `charts/sam-node` values file). You can then drive it with the bundled MCP client: @@ -127,17 +129,11 @@ You can then drive it with the bundled MCP client: To verify the full discovery-and-call path against a freshly built mesh: ```bash +make kind-up ARGS="-s" make kind-e2e-mesh ``` -This enrolls a local node, waits for it to discover `mcp://calculator/add`, calls `add(2, 3)`, and asserts the result is `5`. Because nodes ship bare by default, the check needs a `calc-mcp` service on the mesh — bring the mesh up with the bundled e2e layout, which pins it: - -```bash -MESH_CONFIG=development/kind/mesh-config.e2e.yaml make kind-up ARGS="-s" -make kind-e2e-mesh -``` - -`MESH_CONFIG` overrides which layout `make kind-up` deploys (default: `mesh-config.yaml`); `mesh-config.e2e.yaml` assigns `calc-mcp` to `node-b`. +`kind-e2e-mesh` deploys `calc-mcp` as a `charts/sam-node` release, enrolls a local node, waits for it to discover `mcp://calculator/add`, calls `add(2, 3)`, and asserts the result is `5`. --- diff --git a/site/content/docs/use-cases/gemini-buddy.md b/site/content/docs/use-cases/gemini-buddy.md index b309328b..b9a592dc 100644 --- a/site/content/docs/use-cases/gemini-buddy.md +++ b/site/content/docs/use-cases/gemini-buddy.md @@ -79,20 +79,21 @@ The buddy shells out to the Gemini CLI, so set your API key on the API-key `ENV` line in `development/examples/gemini-buddy-mcp/Dockerfile` before building (a free Google AI Studio key is fine for the demo). -### 2. Mesh layout +### 2. Bring the mesh up and deploy the buddy -Host the buddy on one node in `development/kind/mesh-config.yaml`: - -```yaml -node-a: # bare node (orchestrator entry) -node-b: gemini-buddy-mcp # the buddy +```bash +make build # builds ./bin/sam-node (once) +make kind-up # control plane + router (no sam-nodes yet) +docker build -t gemini-buddy-mcp:local development/examples/gemini-buddy-mcp +kind load docker-image --name sam-kind gemini-buddy-mcp:local +helm --kube-context kind-sam-kind -n sam-kind install gemini-buddy charts/sam-node \ + -f development/kind/sam-node.values.yaml \ + -f development/examples/gemini-buddy-mcp/values.yaml ``` -### 3. Bring the mesh up and start a local orchestrator node +### 3. Start a local orchestrator node ```bash -make build # builds ./bin/sam-node (once) -make kind-up # control plane + router + buddy (node-b) make kind-local-node # local sam-node enrolled in the mesh — LEAVE RUNNING ``` diff --git a/site/content/docs/use-cases/warm-agent-pool.md b/site/content/docs/use-cases/warm-agent-pool.md index f191bd7e..38102ec9 100644 --- a/site/content/docs/use-cases/warm-agent-pool.md +++ b/site/content/docs/use-cases/warm-agent-pool.md @@ -106,39 +106,33 @@ The reviewer workers shell out to an LLM, so set your API key on the API-key `ENV` line in `development/examples/code-reviewer-pool/reviewer/Dockerfile` before building (a free key is fine for the demo). -### 2. Mesh layout - -This use case needs five node slots, and the cluster ships with two (`node-a` -and `node-b`). Add the three missing workers to -`development/kind/kind-config.yaml` first: - -```yaml - - role: worker - labels: - sam-role: node-c - - role: worker - labels: - sam-role: node-d - - role: worker - labels: - sam-role: node-e +### 2. Bring the mesh up and deploy the pool + +```bash +make build # builds ./bin/sam-node (once) +make kind-up # control plane + router (no sam-nodes yet) ``` -Then write this layout into `development/kind/mesh-config.yaml`: +Then build the two images and deploy the pool as `charts/sam-node` releases — +three reviewer replicas (each replica enrolls as its own mesh node, so the +pool is three same-named `code-reviewer` services) and one manager: -```yaml -node-a: # bare node (orchestrator entry) -node-b: code-reviewer-pool/reviewer # worker -node-c: code-reviewer-pool/reviewer # worker -node-d: code-reviewer-pool/reviewer # worker -node-e: code-reviewer-pool/manager # manager +```bash +docker build -t reviewer:local development/examples/code-reviewer-pool/reviewer +docker build -t pool-manager:local development/examples/code-reviewer-pool/manager +kind load docker-image --name sam-kind reviewer:local pool-manager:local + +helm --kube-context kind-sam-kind -n sam-kind install reviewers charts/sam-node \ + -f development/kind/sam-node.values.yaml \ + -f development/examples/code-reviewer-pool/reviewer/values.yaml --set replicaCount=3 +helm --kube-context kind-sam-kind -n sam-kind install manager charts/sam-node \ + -f development/kind/sam-node.values.yaml \ + -f development/examples/code-reviewer-pool/manager/values.yaml ``` -### 3. Bring the mesh up and start a local orchestrator node +### 3. Start a local orchestrator node ```bash -make build # builds ./bin/sam-node (once) -make kind-up # control plane + router + reviewer pool (node-b/c/d) + manager (node-e) make kind-local-node # local sam-node enrolled in the mesh — LEAVE RUNNING ``` @@ -199,13 +193,13 @@ in the pool. ### 6. Elasticity beat (add a worker mid-job) -Scale a reviewer down, start a larger job, then scale it back up — the manager -picks the new worker up on its next discovery pass and starts leasing it: +Scale the reviewer pool down, start a larger job, then scale it back up — the +manager picks the new worker up on its next discovery pass and starts leasing it: ```bash -kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=0 +kubectl --context kind-sam-kind -n sam-kind scale deploy/reviewers-sam-node --replicas=2 # start a big job, then: -kubectl --context kind-sam-kind -n sam-kind scale deploy/node-d --replicas=1 +kubectl --context kind-sam-kind -n sam-kind scale deploy/reviewers-sam-node --replicas=3 ``` ## Configuration diff --git a/site/content/docs/user/kubernetes-deployment.md b/site/content/docs/user/kubernetes-deployment.md index 55c54f60..68c8fba1 100644 --- a/site/content/docs/user/kubernetes-deployment.md +++ b/site/content/docs/user/kubernetes-deployment.md @@ -245,6 +245,12 @@ Refer to [dns-sync-cronjob-template.yaml](https://github.com/google/sam/blob/mai To secure nodes without distributing static passwords, configure nodes to authenticate via **ServiceAccount projected tokens** (Workload Identity Federation). +> [!TIP] +> The repository ships this pattern as a Helm chart: `charts/sam-node` +> renders the ServiceAccount, config ConfigMap and Deployment below from a +> small values file (see the chart's README). The manifests that follow show +> what it produces. + ### 1. Create a ServiceAccount ```yaml apiVersion: v1 From daa517d07bc60aeb5a5b1693665ca1fc065fe3c8 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 15:47:57 +0000 Subject: [PATCH 06/18] kind, charts: guard the e2e helm fallback and fix the README config wording --- charts/sam-node/README.md | 7 ++++--- development/kind/test-mesh-e2e.sh | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/charts/sam-node/README.md b/charts/sam-node/README.md index 13324ac3..6c1f7893 100644 --- a/charts/sam-node/README.md +++ b/charts/sam-node/README.md @@ -40,8 +40,9 @@ in a base file and the service description in its own, then pass both: The service container and the node share the pod's network, so `target_url` points at `127.0.0.1:`. Services declared in `config.services` are advertised to the mesh via DHT and gossip; `config.attenuation` narrows what -the node's credential permits. The config file is read once at node startup — -the chart rolls the pods on config changes (checksum annotation). +the node's credential permits. Your `config` values are merged over the chart's +defaults and rendered as `sam-node.yaml` at startup — the chart rolls the pods +on config changes (checksum annotation). ## Values @@ -52,7 +53,7 @@ the chart rolls the pods on config changes (checksum annotation). | `apiToken` | `devtoken` | Bearer token for the node's local REST API | | `bindAddr` | `127.0.0.1:8080` | Node API bind address (loopback = pod-private) | | `extraArgs` | `[]` | Extra sam-node args | -| `config` | empty services | Rendered verbatim as `sam-node.yaml` | +| `config` | empty services | Merged over the chart's defaults and rendered as `sam-node.yaml` | | `service.image` | `""` | Service container image; empty = bare node | | `service.name/command/env/ports/resources` | — | Service container spec | | `image.repository/tag/pullPolicy` | `sam-node:local` | Node image | diff --git a/development/kind/test-mesh-e2e.sh b/development/kind/test-mesh-e2e.sh index 83568485..7cf1f4b5 100755 --- a/development/kind/test-mesh-e2e.sh +++ b/development/kind/test-mesh-e2e.sh @@ -20,7 +20,10 @@ NAMESPACE="sam-kind" # Same helm fallback as run.sh: CI and dev boxes may only have ./bin/helm. HELM="helm" -command -v helm >/dev/null 2>&1 || HELM="./bin/helm" +if ! command -v helm >/dev/null 2>&1; then + [[ -x "./bin/helm" ]] || { echo "missing prerequisite: helm (install helm or place it in ./bin/helm)" >&2; exit 1; } + HELM="./bin/helm" +fi echo "== Deploying calc-mcp via charts/sam-node ==" docker build -t calc-mcp:local development/examples/calc-mcp From 3eeca72f53e69cee2f0645dd56962c5e31eeea89 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 20:29:56 +0000 Subject: [PATCH 07/18] examples: migrate chat-a2a to the charts/sam-node flow --- development/examples/chat-a2a/README.md | 20 ++++++++----------- .../examples/chat-a2a/sam-node-config.yaml | 8 -------- development/examples/chat-a2a/values.yaml | 12 +++++++++++ site/content/docs/use-cases/chat-a2a.md | 19 +++++++++--------- 4 files changed, 30 insertions(+), 29 deletions(-) delete mode 100644 development/examples/chat-a2a/sam-node-config.yaml create mode 100644 development/examples/chat-a2a/values.yaml diff --git a/development/examples/chat-a2a/README.md b/development/examples/chat-a2a/README.md index 5d989f61..9820ab88 100644 --- a/development/examples/chat-a2a/README.md +++ b/development/examples/chat-a2a/README.md @@ -10,20 +10,16 @@ Edit `Dockerfile` and replace `` in `ENV GEMINI_API_KEY=` (same pattern as `gemini-buddy-mcp`). Optionally override the model with `GEMINI_MODEL` (default `models/gemini-3.5-flash-lite`). -## 2. Host the agent on a mesh node - -Assign it in `development/kind/mesh-config.yaml`: - -```yaml -node-a: -node-b: chat-a2a -``` - -Then bring the mesh up: +## 2. Bring the mesh up and deploy the agent ```sh -make build -make kind-up +make build # builds ./bin/sam-node (once) +make kind-up # control plane + router (no sam-nodes yet) +docker build -t chat-a2a:local development/examples/chat-a2a +kind load docker-image --name sam-kind chat-a2a:local +helm --kube-context kind-sam-kind -n sam-kind install chat-a2a charts/sam-node \ + -f development/kind/sam-node.values.yaml \ + -f development/examples/chat-a2a/values.yaml ``` ## 3. Enroll a local caller node diff --git a/development/examples/chat-a2a/sam-node-config.yaml b/development/examples/chat-a2a/sam-node-config.yaml deleted file mode 100644 index 910a4901..00000000 --- a/development/examples/chat-a2a/sam-node-config.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: "v1alpha1" -attenuation: - policies: [] -services: - - type: "a2a" - name: "chat" - description: "Gemini-backed conversational A2A agent" - target_url: "http://127.0.0.1:7777" diff --git a/development/examples/chat-a2a/values.yaml b/development/examples/chat-a2a/values.yaml new file mode 100644 index 00000000..aa5d442c --- /dev/null +++ b/development/examples/chat-a2a/values.yaml @@ -0,0 +1,12 @@ +config: + version: v1alpha1 + attenuation: + policies: [] + services: + - type: a2a + name: chat + description: Gemini-backed conversational A2A agent + target_url: http://127.0.0.1:7777 +service: + name: chat-a2a + image: chat-a2a:local diff --git a/site/content/docs/use-cases/chat-a2a.md b/site/content/docs/use-cases/chat-a2a.md index 028d8590..0eb1e4a7 100644 --- a/site/content/docs/use-cases/chat-a2a.md +++ b/site/content/docs/use-cases/chat-a2a.md @@ -80,20 +80,21 @@ that brings the agent up with one command. line in `development/examples/chat-a2a/Dockerfile` before building (a free Google AI Studio key is fine for the demo). -### 2. Mesh layout +### 2. Bring the mesh up and deploy the agent -Host the agent on one node in `development/kind/mesh-config.yaml`: - -```yaml -node-a: # bare node -node-b: chat-a2a # the A2A agent +```bash +make build # builds ./bin/sam-node (once) +make kind-up # control plane + router (no sam-nodes yet) +docker build -t chat-a2a:local development/examples/chat-a2a +kind load docker-image --name sam-kind chat-a2a:local +helm --kube-context kind-sam-kind -n sam-kind install chat-a2a charts/sam-node \ + -f development/kind/sam-node.values.yaml \ + -f development/examples/chat-a2a/values.yaml ``` -### 3. Bring the mesh up and enroll a local caller node +### 3. Enroll a local caller node ```bash -make build # builds ./bin/sam-node (once) -make kind-up # control plane + router + agent (node-b) make kind-local-node # local sam-node enrolled in the mesh — LEAVE RUNNING ``` From 341f77fd608aba318a7b582066d32f08d9322186 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 20:53:58 +0000 Subject: [PATCH 08/18] kind: add deploy-kind-example.sh wrapping the example deploy commands --- development/kind/deploy-kind-example.sh | 27 +++++++++++++++++++ development/kind/run.sh | 6 ++--- development/kind/test-mesh-e2e.sh | 17 +----------- .../docs/development/kubernetes-deployment.md | 10 +++++++ 4 files changed, 40 insertions(+), 20 deletions(-) create mode 100755 development/kind/deploy-kind-example.sh diff --git a/development/kind/deploy-kind-example.sh b/development/kind/deploy-kind-example.sh new file mode 100755 index 00000000..36a534d8 --- /dev/null +++ b/development/kind/deploy-kind-example.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Build, load and install a development/examples/ into the kind mesh. +# Extra args pass to helm, e.g.: deploy-kind-example.sh code-reviewer-pool/reviewer --set replicaCount=3 +set -euo pipefail + +[[ $# -ge 1 ]] || { echo "usage: $(basename "$0") [helm args...]" >&2; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +EXAMPLE="$1"; shift +DIR="${PROJECT_ROOT}/development/examples/${EXAMPLE}" +[[ -f "${DIR}/values.yaml" ]] || { echo "example '${EXAMPLE}' not found (no values.yaml in development/examples/${EXAMPLE})" >&2; exit 1; } +NAME="$(basename "${EXAMPLE}")" + +HELM="helm" +if ! command -v helm >/dev/null 2>&1; then + [[ -x "${PROJECT_ROOT}/bin/helm" ]] || { echo "missing prerequisite: helm (install helm or place it in ./bin/helm)" >&2; exit 1; } + HELM="${PROJECT_ROOT}/bin/helm" +fi + +set -x +docker build -t "${NAME}:local" "${DIR}" +kind load docker-image --name sam-kind "${NAME}:local" +"${HELM}" --kube-context kind-sam-kind -n sam-kind upgrade --install "${NAME}" "${PROJECT_ROOT}/charts/sam-node" \ + -f "${PROJECT_ROOT}/development/kind/sam-node.values.yaml" \ + -f "${DIR}/values.yaml" "$@" +kubectl --context kind-sam-kind -n sam-kind rollout status "deployment/${NAME}-sam-node" --timeout=180s diff --git a/development/kind/run.sh b/development/kind/run.sh index 424ec42f..d3190ed8 100755 --- a/development/kind/run.sh +++ b/development/kind/run.sh @@ -267,10 +267,8 @@ echo " control plane: http://${MAIN_IP}" echo " dex: ${OIDC_ISSUER}" echo echo "To put a service on the mesh, deploy an example with charts/sam-node:" -echo " docker build -t calc-mcp:local development/examples/calc-mcp" -echo " kind load docker-image --name ${CLUSTER} calc-mcp:local" -echo " ${HELM} --kube-context ${KCTX} -n ${NAMESPACE} install calc-mcp charts/sam-node \\" -echo " -f development/kind/sam-node.values.yaml -f development/examples/calc-mcp/values.yaml" +echo " ./development/kind/deploy-kind-example.sh calc-mcp" +echo "(it prints the docker build / kind load / helm install commands as it runs them)" echo echo "To drive the mesh, enroll a local node in another shell (it stays in the foreground):" echo " make build && make kind-local-node" diff --git a/development/kind/test-mesh-e2e.sh b/development/kind/test-mesh-e2e.sh index 7cf1f4b5..86f0ca8a 100755 --- a/development/kind/test-mesh-e2e.sh +++ b/development/kind/test-mesh-e2e.sh @@ -15,23 +15,8 @@ set -euo pipefail -KCTX="kind-sam-kind" -NAMESPACE="sam-kind" - -# Same helm fallback as run.sh: CI and dev boxes may only have ./bin/helm. -HELM="helm" -if ! command -v helm >/dev/null 2>&1; then - [[ -x "./bin/helm" ]] || { echo "missing prerequisite: helm (install helm or place it in ./bin/helm)" >&2; exit 1; } - HELM="./bin/helm" -fi - echo "== Deploying calc-mcp via charts/sam-node ==" -docker build -t calc-mcp:local development/examples/calc-mcp -kind load docker-image --name sam-kind calc-mcp:local -"${HELM}" --kube-context "${KCTX}" -n "${NAMESPACE}" install calc-mcp charts/sam-node \ - -f development/kind/sam-node.values.yaml \ - -f development/examples/calc-mcp/values.yaml -kubectl --context "${KCTX}" -n "${NAMESPACE}" rollout status deployment/calc-mcp-sam-node --timeout=180s +./development/kind/deploy-kind-example.sh calc-mcp # If running locally, we might want to store logs in a temp dir LOG_DIR="${RUNNER_TEMP:-$(mktemp -d)}" diff --git a/site/content/docs/development/kubernetes-deployment.md b/site/content/docs/development/kubernetes-deployment.md index e6077e3b..3e12158b 100644 --- a/site/content/docs/development/kubernetes-deployment.md +++ b/site/content/docs/development/kubernetes-deployment.md @@ -69,6 +69,16 @@ helm --kube-context kind-sam-kind -n sam-kind install calc-mcp charts/sam-node \ -f development/examples/calc-mcp/values.yaml ``` +`development/kind/deploy-kind-example.sh` wraps those commands (as +`helm upgrade --install`, plus a rollout wait) and echoes each one as it +runs, so deploying — or redeploying after a code change — is one line. +Extra args pass through to helm: + +```bash +./development/kind/deploy-kind-example.sh calc-mcp +./development/kind/deploy-kind-example.sh code-reviewer-pool/reviewer --set replicaCount=3 +``` + To write your own service, copy an example folder: a backend listening on a local port, a `Dockerfile`, and a `values.yaml` declaring the service — From 796bca82588cf7b5e24fde9c4e5685c22c290165 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 21:04:40 +0000 Subject: [PATCH 09/18] kind: deploy-kind-service.sh takes any service directory, moves to development/ --- ...kind-example.sh => deploy-kind-service.sh} | 21 ++++++++++++------- development/kind/run.sh | 2 +- development/kind/test-mesh-e2e.sh | 2 +- .../docs/development/kubernetes-deployment.md | 12 ++++++----- 4 files changed, 22 insertions(+), 15 deletions(-) rename development/{kind/deploy-kind-example.sh => deploy-kind-service.sh} (50%) diff --git a/development/kind/deploy-kind-example.sh b/development/deploy-kind-service.sh similarity index 50% rename from development/kind/deploy-kind-example.sh rename to development/deploy-kind-service.sh index 36a534d8..386fb8f6 100755 --- a/development/kind/deploy-kind-example.sh +++ b/development/deploy-kind-service.sh @@ -1,16 +1,21 @@ #!/usr/bin/env bash -# Build, load and install a development/examples/ into the kind mesh. -# Extra args pass to helm, e.g.: deploy-kind-example.sh code-reviewer-pool/reviewer --set replicaCount=3 +# Build, load and install a service into the kind mesh. Takes an example name +# under development/examples/ or a path to any directory with a Dockerfile and +# a charts/sam-node values.yaml. Extra args pass to helm, e.g. --set replicaCount=3 set -euo pipefail -[[ $# -ge 1 ]] || { echo "usage: $(basename "$0") [helm args...]" >&2; exit 1; } +[[ $# -ge 1 ]] || { echo "usage: $(basename "$0") [helm args...]" >&2; exit 1; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" -EXAMPLE="$1"; shift -DIR="${PROJECT_ROOT}/development/examples/${EXAMPLE}" -[[ -f "${DIR}/values.yaml" ]] || { echo "example '${EXAMPLE}' not found (no values.yaml in development/examples/${EXAMPLE})" >&2; exit 1; } -NAME="$(basename "${EXAMPLE}")" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +SERVICE="$1"; shift +if [[ -d "${SERVICE}" ]]; then + DIR="$(cd "${SERVICE}" && pwd)" +else + DIR="${PROJECT_ROOT}/development/examples/${SERVICE}" +fi +[[ -f "${DIR}/values.yaml" ]] || { echo "no values.yaml in ${DIR} (pass an example name under development/examples/ or a path to a service directory)" >&2; exit 1; } +NAME="$(basename "${DIR}")" HELM="helm" if ! command -v helm >/dev/null 2>&1; then diff --git a/development/kind/run.sh b/development/kind/run.sh index d3190ed8..775d221f 100755 --- a/development/kind/run.sh +++ b/development/kind/run.sh @@ -267,7 +267,7 @@ echo " control plane: http://${MAIN_IP}" echo " dex: ${OIDC_ISSUER}" echo echo "To put a service on the mesh, deploy an example with charts/sam-node:" -echo " ./development/kind/deploy-kind-example.sh calc-mcp" +echo " ./development/deploy-kind-service.sh calc-mcp" echo "(it prints the docker build / kind load / helm install commands as it runs them)" echo echo "To drive the mesh, enroll a local node in another shell (it stays in the foreground):" diff --git a/development/kind/test-mesh-e2e.sh b/development/kind/test-mesh-e2e.sh index 86f0ca8a..2aad7ab3 100755 --- a/development/kind/test-mesh-e2e.sh +++ b/development/kind/test-mesh-e2e.sh @@ -16,7 +16,7 @@ set -euo pipefail echo "== Deploying calc-mcp via charts/sam-node ==" -./development/kind/deploy-kind-example.sh calc-mcp +./development/deploy-kind-service.sh calc-mcp # If running locally, we might want to store logs in a temp dir LOG_DIR="${RUNNER_TEMP:-$(mktemp -d)}" diff --git a/site/content/docs/development/kubernetes-deployment.md b/site/content/docs/development/kubernetes-deployment.md index 3e12158b..178972cb 100644 --- a/site/content/docs/development/kubernetes-deployment.md +++ b/site/content/docs/development/kubernetes-deployment.md @@ -69,14 +69,16 @@ helm --kube-context kind-sam-kind -n sam-kind install calc-mcp charts/sam-node \ -f development/examples/calc-mcp/values.yaml ``` -`development/kind/deploy-kind-example.sh` wraps those commands (as +`development/deploy-kind-service.sh` wraps those commands (as `helm upgrade --install`, plus a rollout wait) and echoes each one as it -runs, so deploying — or redeploying after a code change — is one line. -Extra args pass through to helm: +runs, so deploying — or redeploying after a code change — is one line. It +takes an example name or a path to any directory holding a `Dockerfile` and +a `values.yaml`, and extra args pass through to helm: ```bash -./development/kind/deploy-kind-example.sh calc-mcp -./development/kind/deploy-kind-example.sh code-reviewer-pool/reviewer --set replicaCount=3 +./development/deploy-kind-service.sh calc-mcp +./development/deploy-kind-service.sh code-reviewer-pool/reviewer --set replicaCount=3 +./development/deploy-kind-service.sh ~/src/my-service ``` To write your own service, copy an example folder: a backend listening on a From a818382885828df6230a7f44424443a179dbd388 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 21:06:14 +0000 Subject: [PATCH 10/18] kind: deploy-kind-service.sh takes a plain directory path only --- development/deploy-kind-service.sh | 18 +++++++----------- development/kind/run.sh | 2 +- development/kind/test-mesh-e2e.sh | 2 +- .../docs/development/kubernetes-deployment.md | 8 ++++---- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/development/deploy-kind-service.sh b/development/deploy-kind-service.sh index 386fb8f6..379367cb 100755 --- a/development/deploy-kind-service.sh +++ b/development/deploy-kind-service.sh @@ -1,20 +1,16 @@ #!/usr/bin/env bash -# Build, load and install a service into the kind mesh. Takes an example name -# under development/examples/ or a path to any directory with a Dockerfile and -# a charts/sam-node values.yaml. Extra args pass to helm, e.g. --set replicaCount=3 +# Build, load and install a service into the kind mesh from a directory holding +# a Dockerfile and a charts/sam-node values.yaml (e.g. development/examples/calc-mcp). +# Extra args pass to helm, e.g. --set replicaCount=3 set -euo pipefail -[[ $# -ge 1 ]] || { echo "usage: $(basename "$0") [helm args...]" >&2; exit 1; } +[[ $# -ge 1 ]] || { echo "usage: $(basename "$0") [helm args...]" >&2; exit 1; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -SERVICE="$1"; shift -if [[ -d "${SERVICE}" ]]; then - DIR="$(cd "${SERVICE}" && pwd)" -else - DIR="${PROJECT_ROOT}/development/examples/${SERVICE}" -fi -[[ -f "${DIR}/values.yaml" ]] || { echo "no values.yaml in ${DIR} (pass an example name under development/examples/ or a path to a service directory)" >&2; exit 1; } +[[ -d "$1" ]] || { echo "not a directory: $1" >&2; exit 1; } +DIR="$(cd "$1" && pwd)"; shift +[[ -f "${DIR}/values.yaml" ]] || { echo "no values.yaml in ${DIR}" >&2; exit 1; } NAME="$(basename "${DIR}")" HELM="helm" diff --git a/development/kind/run.sh b/development/kind/run.sh index 775d221f..f6d7cdc8 100755 --- a/development/kind/run.sh +++ b/development/kind/run.sh @@ -267,7 +267,7 @@ echo " control plane: http://${MAIN_IP}" echo " dex: ${OIDC_ISSUER}" echo echo "To put a service on the mesh, deploy an example with charts/sam-node:" -echo " ./development/deploy-kind-service.sh calc-mcp" +echo " ./development/deploy-kind-service.sh development/examples/calc-mcp" echo "(it prints the docker build / kind load / helm install commands as it runs them)" echo echo "To drive the mesh, enroll a local node in another shell (it stays in the foreground):" diff --git a/development/kind/test-mesh-e2e.sh b/development/kind/test-mesh-e2e.sh index 2aad7ab3..ce38e7aa 100755 --- a/development/kind/test-mesh-e2e.sh +++ b/development/kind/test-mesh-e2e.sh @@ -16,7 +16,7 @@ set -euo pipefail echo "== Deploying calc-mcp via charts/sam-node ==" -./development/deploy-kind-service.sh calc-mcp +./development/deploy-kind-service.sh development/examples/calc-mcp # If running locally, we might want to store logs in a temp dir LOG_DIR="${RUNNER_TEMP:-$(mktemp -d)}" diff --git a/site/content/docs/development/kubernetes-deployment.md b/site/content/docs/development/kubernetes-deployment.md index 178972cb..afd71acf 100644 --- a/site/content/docs/development/kubernetes-deployment.md +++ b/site/content/docs/development/kubernetes-deployment.md @@ -72,12 +72,12 @@ helm --kube-context kind-sam-kind -n sam-kind install calc-mcp charts/sam-node \ `development/deploy-kind-service.sh` wraps those commands (as `helm upgrade --install`, plus a rollout wait) and echoes each one as it runs, so deploying — or redeploying after a code change — is one line. It -takes an example name or a path to any directory holding a `Dockerfile` and -a `values.yaml`, and extra args pass through to helm: +takes a path to any directory holding a `Dockerfile` and a `values.yaml`, +and extra args pass through to helm: ```bash -./development/deploy-kind-service.sh calc-mcp -./development/deploy-kind-service.sh code-reviewer-pool/reviewer --set replicaCount=3 +./development/deploy-kind-service.sh development/examples/calc-mcp +./development/deploy-kind-service.sh development/examples/code-reviewer-pool/reviewer --set replicaCount=3 ./development/deploy-kind-service.sh ~/src/my-service ``` From 5ffef1da698c98e5089963d586720fd724fec3b1 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 21:13:58 +0000 Subject: [PATCH 11/18] kind: RELEASE env override lets deploy-kind-service.sh deploy one directory as several nodes --- development/deploy-kind-service.sh | 8 +++++--- site/content/docs/development/kubernetes-deployment.md | 3 +++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/development/deploy-kind-service.sh b/development/deploy-kind-service.sh index 379367cb..0a0c08f2 100755 --- a/development/deploy-kind-service.sh +++ b/development/deploy-kind-service.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Build, load and install a service into the kind mesh from a directory holding # a Dockerfile and a charts/sam-node values.yaml (e.g. development/examples/calc-mcp). -# Extra args pass to helm, e.g. --set replicaCount=3 +# Extra args pass to helm (e.g. --set replicaCount=3); RELEASE overrides the +# release name to deploy the same directory as several nodes. set -euo pipefail [[ $# -ge 1 ]] || { echo "usage: $(basename "$0") [helm args...]" >&2; exit 1; } @@ -12,6 +13,7 @@ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" DIR="$(cd "$1" && pwd)"; shift [[ -f "${DIR}/values.yaml" ]] || { echo "no values.yaml in ${DIR}" >&2; exit 1; } NAME="$(basename "${DIR}")" +RELEASE="${RELEASE:-${NAME}}" HELM="helm" if ! command -v helm >/dev/null 2>&1; then @@ -22,7 +24,7 @@ fi set -x docker build -t "${NAME}:local" "${DIR}" kind load docker-image --name sam-kind "${NAME}:local" -"${HELM}" --kube-context kind-sam-kind -n sam-kind upgrade --install "${NAME}" "${PROJECT_ROOT}/charts/sam-node" \ +"${HELM}" --kube-context kind-sam-kind -n sam-kind upgrade --install "${RELEASE}" "${PROJECT_ROOT}/charts/sam-node" \ -f "${PROJECT_ROOT}/development/kind/sam-node.values.yaml" \ -f "${DIR}/values.yaml" "$@" -kubectl --context kind-sam-kind -n sam-kind rollout status "deployment/${NAME}-sam-node" --timeout=180s +kubectl --context kind-sam-kind -n sam-kind rollout status "deployment/${RELEASE}-sam-node" --timeout=180s diff --git a/site/content/docs/development/kubernetes-deployment.md b/site/content/docs/development/kubernetes-deployment.md index afd71acf..3cb4e5a9 100644 --- a/site/content/docs/development/kubernetes-deployment.md +++ b/site/content/docs/development/kubernetes-deployment.md @@ -79,6 +79,9 @@ and extra args pass through to helm: ./development/deploy-kind-service.sh development/examples/calc-mcp ./development/deploy-kind-service.sh development/examples/code-reviewer-pool/reviewer --set replicaCount=3 ./development/deploy-kind-service.sh ~/src/my-service +# same service as a second, differently-labeled node: +RELEASE=calc-b ./development/deploy-kind-service.sh development/examples/calc-mcp \ + --set-json 'extraArgs=["--discovery-interval=200ms","--labels=region=us-east-1"]' ``` To write your own service, copy an example folder: a backend listening on a From 420855f177fb674b63bcdc3780cd3697f972261c Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 21:15:33 +0000 Subject: [PATCH 12/18] kind: replace the RELEASE env with a --release-name flag --- development/deploy-kind-service.sh | 19 ++++++++++++++----- .../docs/development/kubernetes-deployment.md | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/development/deploy-kind-service.sh b/development/deploy-kind-service.sh index 0a0c08f2..3f7cfc2e 100755 --- a/development/deploy-kind-service.sh +++ b/development/deploy-kind-service.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # Build, load and install a service into the kind mesh from a directory holding # a Dockerfile and a charts/sam-node values.yaml (e.g. development/examples/calc-mcp). -# Extra args pass to helm (e.g. --set replicaCount=3); RELEASE overrides the -# release name to deploy the same directory as several nodes. +# --release-name deploys the same directory as several nodes; anything else +# after the directory passes to helm (e.g. --set replicaCount=3). set -euo pipefail -[[ $# -ge 1 ]] || { echo "usage: $(basename "$0") [helm args...]" >&2; exit 1; } +[[ $# -ge 1 ]] || { echo "usage: $(basename "$0") [--release-name ] [helm args...]" >&2; exit 1; } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" @@ -13,7 +13,16 @@ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" DIR="$(cd "$1" && pwd)"; shift [[ -f "${DIR}/values.yaml" ]] || { echo "no values.yaml in ${DIR}" >&2; exit 1; } NAME="$(basename "${DIR}")" -RELEASE="${RELEASE:-${NAME}}" + +RELEASE="${NAME}" +HELM_ARGS=() +while [[ $# -gt 0 ]]; do + case "$1" in + --release-name) [[ $# -ge 2 ]] || { echo "--release-name needs a value" >&2; exit 1; }; RELEASE="$2"; shift 2 ;; + --release-name=*) RELEASE="${1#*=}"; shift ;; + *) HELM_ARGS+=("$1"); shift ;; + esac +done HELM="helm" if ! command -v helm >/dev/null 2>&1; then @@ -26,5 +35,5 @@ docker build -t "${NAME}:local" "${DIR}" kind load docker-image --name sam-kind "${NAME}:local" "${HELM}" --kube-context kind-sam-kind -n sam-kind upgrade --install "${RELEASE}" "${PROJECT_ROOT}/charts/sam-node" \ -f "${PROJECT_ROOT}/development/kind/sam-node.values.yaml" \ - -f "${DIR}/values.yaml" "$@" + -f "${DIR}/values.yaml" ${HELM_ARGS[@]+"${HELM_ARGS[@]}"} kubectl --context kind-sam-kind -n sam-kind rollout status "deployment/${RELEASE}-sam-node" --timeout=180s diff --git a/site/content/docs/development/kubernetes-deployment.md b/site/content/docs/development/kubernetes-deployment.md index 3cb4e5a9..588da9d3 100644 --- a/site/content/docs/development/kubernetes-deployment.md +++ b/site/content/docs/development/kubernetes-deployment.md @@ -80,7 +80,7 @@ and extra args pass through to helm: ./development/deploy-kind-service.sh development/examples/code-reviewer-pool/reviewer --set replicaCount=3 ./development/deploy-kind-service.sh ~/src/my-service # same service as a second, differently-labeled node: -RELEASE=calc-b ./development/deploy-kind-service.sh development/examples/calc-mcp \ +./development/deploy-kind-service.sh development/examples/calc-mcp --release-name calc-b \ --set-json 'extraArgs=["--discovery-interval=200ms","--labels=region=us-east-1"]' ``` From b1b8442ca2bfa9b25b1b9fb651b7205bd6b49d88 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Fri, 4 Sep 2026 22:14:22 +0000 Subject: [PATCH 13/18] kind: run the local node with a throwaway data-dir so stale identities never block enrollment --- development/kind/run-local-node.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/development/kind/run-local-node.sh b/development/kind/run-local-node.sh index 5814ffae..369f954b 100755 --- a/development/kind/run-local-node.sh +++ b/development/kind/run-local-node.sh @@ -41,12 +41,22 @@ BOOTSTRAP_TOKEN="$(printf '%s' "${TOKEN_RESPONSE}" | jq -r '.token // empty' 2>/ echo "Enrolling local ./bin/sam-node into the mesh control plane at ${CONTROL_PLANE_URL}…" echo " MCP/sidecar API on 127.0.0.1:9099" + +# Throwaway identity per run: the default data-dir keeps the biscuit from a +# previous cluster, and enrolling with it against a fresh control plane fails. +DATA_DIR="$(mktemp -d)" +cleanup() { [[ -n "${NODE_PID:-}" ]] && kill "${NODE_PID}" 2>/dev/null || true; rm -rf "${DATA_DIR}"; } +trap cleanup EXIT INT TERM + export SAM_API_TOKEN=devtoken -exec ./bin/sam-node run \ +./bin/sam-node run \ --control-plane "${CONTROL_PLANE_URL}" \ --bootstrap-token "${BOOTSTRAP_TOKEN}" \ --listen /ip4/0.0.0.0/tcp/0 \ --bind-addr 127.0.0.1:9099 \ --discovery-interval 200ms \ --router-connect-timeout 10s \ - "$@" + --data-dir "${DATA_DIR}" \ + "$@" & +NODE_PID=$! +wait "${NODE_PID}" From 41a7a91f874989c70dc640af2e16a3ff9b8f4b74 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Mon, 7 Sep 2026 08:17:01 +0000 Subject: [PATCH 14/18] kind: keep sam-nodes off the router worker; same-node hostPort dials fail enrollment --- development/kind/sam-node.values.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/development/kind/sam-node.values.yaml b/development/kind/sam-node.values.yaml index 9c421fcc..edf92310 100644 --- a/development/kind/sam-node.values.yaml +++ b/development/kind/sam-node.values.yaml @@ -2,3 +2,14 @@ # values on top: helm install charts/sam-node -f this-file -f /values.yaml controlPlaneUrl: http://sam-mesh-control-plane:8080 extraArgs: ["--discovery-interval=200ms"] + +# Keep nodes off the router's worker: a pod dialing its own node's +# hostPort 4501 trips kind's portmap hairpin hole and enrollment fails. +affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: sam-role + operator: NotIn + values: ["control-plane"] From fec638e08c42fa42b706b315ecf1f1dfaf98b58c Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Mon, 7 Sep 2026 09:30:34 +0000 Subject: [PATCH 15/18] charts: trim the sam-node README --- charts/sam-node/README.md | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/charts/sam-node/README.md b/charts/sam-node/README.md index 6c1f7893..5fc26ee2 100644 --- a/charts/sam-node/README.md +++ b/charts/sam-node/README.md @@ -31,19 +31,6 @@ service: image: calc-mcp:local ``` -Values files stack: keep environment wiring (`controlPlaneUrl`, extra args) -in a base file and the service description in its own, then pass both: -`helm install calc charts/sam-node -f base.yaml -f calc/values.yaml` -(later files win). The kind dev mesh ships such a base at -`development/kind/sam-node.values.yaml`. - -The service container and the node share the pod's network, so `target_url` -points at `127.0.0.1:`. Services declared in `config.services` are -advertised to the mesh via DHT and gossip; `config.attenuation` narrows what -the node's credential permits. Your `config` values are merged over the chart's -defaults and rendered as `sam-node.yaml` at startup — the chart rolls the pods -on config changes (checksum annotation). - ## Values | Key | Default | Meaning | @@ -53,16 +40,8 @@ on config changes (checksum annotation). | `apiToken` | `devtoken` | Bearer token for the node's local REST API | | `bindAddr` | `127.0.0.1:8080` | Node API bind address (loopback = pod-private) | | `extraArgs` | `[]` | Extra sam-node args | -| `config` | empty services | Merged over the chart's defaults and rendered as `sam-node.yaml` | +| `config` | empty services | Merged over the chart's defaults and rendered as `sam-node.yaml`; pods roll on config changes | | `service.image` | `""` | Service container image; empty = bare node | | `service.name/command/env/ports/resources` | — | Service container spec | | `image.repository/tag/pullPolicy` | `sam-node:local` | Node image | | `replicaCount` | `1` | Each replica enrolls as its own mesh node | - -## Dynamic registration (alternative to `config.services`) - -A running node also accepts `POST /sam/service/register` on its API -(`bindAddr`, bearer `apiToken`) with a JSON body -`{"service":{"type":"mcp","name":"x","description":"…"},"targetUrl":"http://…"}`. -Registrations are in-memory: after a node restart the registrar must -re-register. Static `config.services` entries need no such care. From 271a0fddb7e94b2039f41ed5661c120c08593534 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Mon, 7 Sep 2026 19:30:05 +0000 Subject: [PATCH 16/18] charts: make the sam-node ServiceAccount optional and annotatable --- charts/sam-node/README.md | 1 + charts/sam-node/templates/deployment.yaml | 2 +- charts/sam-node/templates/serviceaccount.yaml | 8 +++++++- charts/sam-node/tests/deployment_test.yaml | 12 ++++++++++++ charts/sam-node/values.yaml | 7 +++++++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/charts/sam-node/README.md b/charts/sam-node/README.md index 5fc26ee2..3818c750 100644 --- a/charts/sam-node/README.md +++ b/charts/sam-node/README.md @@ -43,5 +43,6 @@ service: | `config` | empty services | Merged over the chart's defaults and rendered as `sam-node.yaml`; pods roll on config changes | | `service.image` | `""` | Service container image; empty = bare node | | `service.name/command/env/ports/resources` | — | Service container spec | +| `serviceAccount.create/name/annotations` | `true` / fullname / `{}` | Skip creation, reuse an existing SA, or annotate it (e.g. Workload Identity) | | `image.repository/tag/pullPolicy` | `sam-node:local` | Node image | | `replicaCount` | `1` | Each replica enrolls as its own mesh node | diff --git a/charts/sam-node/templates/deployment.yaml b/charts/sam-node/templates/deployment.yaml index 488d7778..bac61bcd 100644 --- a/charts/sam-node/templates/deployment.yaml +++ b/charts/sam-node/templates/deployment.yaml @@ -18,7 +18,7 @@ spec: # file once at startup and never reloads it. checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} spec: - serviceAccountName: {{ include "sam-node.fullname" . }} + serviceAccountName: {{ default (include "sam-node.fullname" .) .Values.serviceAccount.name }} containers: - name: sam-node image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" diff --git a/charts/sam-node/templates/serviceaccount.yaml b/charts/sam-node/templates/serviceaccount.yaml index 25a24ff8..72b60ef4 100644 --- a/charts/sam-node/templates/serviceaccount.yaml +++ b/charts/sam-node/templates/serviceaccount.yaml @@ -1,6 +1,12 @@ +{{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: - name: {{ include "sam-node.fullname" . }} + name: {{ default (include "sam-node.fullname" .) .Values.serviceAccount.name }} labels: {{- include "sam-node.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/sam-node/tests/deployment_test.yaml b/charts/sam-node/tests/deployment_test.yaml index e087db27..9ea039b1 100644 --- a/charts/sam-node/tests/deployment_test.yaml +++ b/charts/sam-node/tests/deployment_test.yaml @@ -57,3 +57,15 @@ tests: - contains: path: spec.template.spec.containers[0].args content: "--discovery-interval=200ms" + + - it: honors serviceAccount.name for the pod + template: templates/deployment.yaml + set: + controlPlaneUrl: http://sam-mesh-control-plane:8080 + serviceAccount: + create: false + name: existing-sa + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: existing-sa diff --git a/charts/sam-node/values.yaml b/charts/sam-node/values.yaml index 9c66715a..945412a0 100644 --- a/charts/sam-node/values.yaml +++ b/charts/sam-node/values.yaml @@ -44,6 +44,13 @@ service: ports: [] resources: {} +# Set create: false and name to reuse an existing ServiceAccount; annotations +# cover IAM bindings (e.g. GKE Workload Identity). +serviceAccount: + create: true + name: "" + annotations: {} + resources: {} nodeSelector: {} tolerations: [] From 05684f4a258442ab6948f20c6c98201885067c4a Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Mon, 7 Sep 2026 19:30:05 +0000 Subject: [PATCH 17/18] kind: wait on the deployment by label so release names containing sam-node work --- development/deploy-kind-service.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/development/deploy-kind-service.sh b/development/deploy-kind-service.sh index 3f7cfc2e..9f6366e9 100755 --- a/development/deploy-kind-service.sh +++ b/development/deploy-kind-service.sh @@ -36,4 +36,5 @@ kind load docker-image --name sam-kind "${NAME}:local" "${HELM}" --kube-context kind-sam-kind -n sam-kind upgrade --install "${RELEASE}" "${PROJECT_ROOT}/charts/sam-node" \ -f "${PROJECT_ROOT}/development/kind/sam-node.values.yaml" \ -f "${DIR}/values.yaml" ${HELM_ARGS[@]+"${HELM_ARGS[@]}"} -kubectl --context kind-sam-kind -n sam-kind rollout status "deployment/${RELEASE}-sam-node" --timeout=180s +kubectl --context kind-sam-kind -n sam-kind rollout status deployment \ + -l "app.kubernetes.io/name=sam-node,app.kubernetes.io/instance=${RELEASE}" --timeout=180s From c086f1871a9a02b1da4c7d423d1ccc8f3f0f39d6 Mon Sep 17 00:00:00 2001 From: Tomas Tormo Date: Mon, 7 Sep 2026 19:30:05 +0000 Subject: [PATCH 18/18] kind: guard the throwaway data-dir removal --- development/kind/run-local-node.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development/kind/run-local-node.sh b/development/kind/run-local-node.sh index 369f954b..1e6606e4 100755 --- a/development/kind/run-local-node.sh +++ b/development/kind/run-local-node.sh @@ -45,7 +45,7 @@ echo " MCP/sidecar API on 127.0.0.1:9099" # Throwaway identity per run: the default data-dir keeps the biscuit from a # previous cluster, and enrolling with it against a fresh control plane fails. DATA_DIR="$(mktemp -d)" -cleanup() { [[ -n "${NODE_PID:-}" ]] && kill "${NODE_PID}" 2>/dev/null || true; rm -rf "${DATA_DIR}"; } +cleanup() { [[ -n "${NODE_PID:-}" ]] && kill "${NODE_PID}" 2>/dev/null || true; [[ -n "${DATA_DIR:-}" && -d "${DATA_DIR}" ]] && rm -rf "${DATA_DIR}"; } trap cleanup EXIT INT TERM export SAM_API_TOKEN=devtoken