Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2565d52
charts: add sam-node chart for deploying a node with an optional serv…
kaisoz Sep 4, 2026
d25ee42
examples: replace sam-node-config.yaml with charts/sam-node values files
kaisoz Sep 4, 2026
fe43d8d
kind: deploy sam infrastructure only, drop the mesh-config node templ…
kaisoz Sep 4, 2026
de88f4a
e2e: deploy calc-mcp through charts/sam-node
kaisoz Sep 4, 2026
087eef9
docs: kind mesh deploys infra only; services ship via charts/sam-node
kaisoz Sep 4, 2026
daa517d
kind, charts: guard the e2e helm fallback and fix the README config w…
kaisoz Sep 4, 2026
3eeca72
examples: migrate chat-a2a to the charts/sam-node flow
kaisoz Sep 4, 2026
341f77f
kind: add deploy-kind-example.sh wrapping the example deploy commands
kaisoz Sep 4, 2026
796bca8
kind: deploy-kind-service.sh takes any service directory, moves to de…
kaisoz Sep 4, 2026
a818382
kind: deploy-kind-service.sh takes a plain directory path only
kaisoz Sep 4, 2026
5ffef1d
kind: RELEASE env override lets deploy-kind-service.sh deploy one dir…
kaisoz Sep 4, 2026
420855f
kind: replace the RELEASE env with a --release-name flag
kaisoz Sep 4, 2026
b1b8442
kind: run the local node with a throwaway data-dir so stale identitie…
kaisoz Sep 4, 2026
41a7a91
kind: keep sam-nodes off the router worker; same-node hostPort dials …
kaisoz Sep 7, 2026
fec638e
charts: trim the sam-node README
kaisoz Sep 7, 2026
271a0fd
charts: make the sam-node ServiceAccount optional and annotatable
kaisoz Sep 7, 2026
05684f4
kind: wait on the deployment by label so release names containing sam…
kaisoz Sep 7, 2026
c086f18
kind: guard the throwaway data-dir removal
kaisoz Sep 7, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/kind-mesh-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions charts/sam-node/Chart.yaml
Original file line number Diff line number Diff line change
@@ -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"
48 changes: 48 additions & 0 deletions charts/sam-node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 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

| 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 | 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 |
49 changes: 49 additions & 0 deletions charts/sam-node/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -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 }}
9 changes: 9 additions & 0 deletions charts/sam-node/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
91 changes: 91 additions & 0 deletions charts/sam-node/templates/deployment.yaml
Original file line number Diff line number Diff line change
@@ -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: {{ default (include "sam-node.fullname" .) .Values.serviceAccount.name }}
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 }}
12 changes: 12 additions & 0 deletions charts/sam-node/templates/serviceaccount.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ default (include "sam-node.fullname" .) .Values.serviceAccount.name }}
labels:
{{- include "sam-node.labels" . | nindent 4 }}
Comment on lines +2 to +7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To make the Helm chart production-ready and flexible for cloud environments (where ServiceAccounts often require specific annotations for IAM/Workload Identity mapping, or where users might want to reuse an existing ServiceAccount), it is highly recommended to make the ServiceAccount creation optional and support custom annotations.

{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
  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 }}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
71 changes: 71 additions & 0 deletions charts/sam-node/tests/deployment_test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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"
Comment thread
aojea marked this conversation as resolved.
- 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"

- 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
57 changes: 57 additions & 0 deletions charts/sam-node/values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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:<port>.
# Rendered only when image is non-empty.
service:
name: service
image: ""
command: []
env: []
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: {}
Comment thread
kaisoz marked this conversation as resolved.
nodeSelector: {}
tolerations: []
affinity: {}
Loading
Loading