Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
71e472f
charts: expose the router at its node IP, GKE-style
kaisoz Aug 17, 2026
e9a7af4
charts: make sam:role:node's allowed services configurable
kaisoz Aug 17, 2026
f2b76a0
charts: front the mesh with Gateway API
kaisoz Aug 17, 2026
0bffff7
charts: drop the bundled Dex
kaisoz Aug 17, 2026
aade64f
development/kind: deploy the mesh behind the gateway surface
kaisoz Aug 17, 2026
a0cffcc
tests/e2e: pin the chart values the bats lane asserts on
kaisoz Aug 17, 2026
7672f21
docs: describe the gateway-fronted kind mesh
kaisoz Aug 17, 2026
5163c85
development/examples: pin greeter-mcp to mcp 1.x
kaisoz Aug 17, 2026
ca94afa
charts: run the sam pods under the restricted Pod Security Standard
kaisoz Aug 24, 2026
713727f
charts: allow overriding the image tag per image
kaisoz Aug 24, 2026
362c122
charts: roll control plane and console when the shared secret changes
kaisoz Aug 24, 2026
ff8159c
charts: stop granting the bootstrap job access to every secret
kaisoz Aug 24, 2026
313d3a9
charts: warn in NOTES when hostPort caps routers at one per node
kaisoz Aug 24, 2026
06ec530
charts: persist the router key on a PVC
kaisoz Aug 24, 2026
8cf6ff4
charts: add pg_isready probes to the db statefulset
kaisoz Aug 24, 2026
b079565
charts: pass nodeSelector, tolerations and affinity through to every …
kaisoz Aug 24, 2026
64968a7
charts: give the curl init/bootstrap containers requests and limits
kaisoz Aug 24, 2026
7fa1d8a
charts: add helm-unittest template tests
kaisoz Aug 24, 2026
2913960
ci: lint and unit-test the chart on chart changes
kaisoz Aug 24, 2026
e356126
Add helm-template make target. Reduce comments
kaisoz Aug 24, 2026
2ebbe51
tests/e2e: recover the reused cluster from immutable StatefulSet changes
kaisoz Aug 25, 2026
54618a8
charts: derive announced router port from service.port; optional PVC …
kaisoz Aug 25, 2026
678ea5f
charts: disable surge when controlPlane.hostPort is set
kaisoz Aug 25, 2026
e4116b1
Makefile: drop --verify=false from helm plugin install; helm 3 has no…
kaisoz Aug 25, 2026
d7a7066
ci: drop helm-lint from chart-test, test.yaml already runs it
kaisoz Aug 25, 2026
7b3c172
charts: let the bootstrap job update the router-token secret
kaisoz Aug 25, 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
44 changes: 44 additions & 0 deletions .github/workflows/chart-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: chart-test

on:
push:
branches:
- "main"
paths:
- "charts/**"
- ".github/workflows/chart-test.yml"
pull_request:
branches: [main]
paths:
- "charts/**"
- ".github/workflows/chart-test.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
chart_test:
runs-on: ubuntu-22.04
name: Chart lint and unit tests
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# helm-lint already runs in test.yaml
- name: Run chart unit tests
run: make helm-test
14 changes: 13 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ kind-logs:
./development/kind/run.sh -l

kind-down:
kind delete cluster --name sam-kind
./development/kind/run.sh -d

.PHONY: kind-local-node
kind-local-node:
Expand Down Expand Up @@ -194,9 +194,21 @@ helm-lint:
fi; \
$$HELM_BIN lint ./charts/sam-mesh

# render the chart to bin/chart/ for inspection; pass extra flags via ARGS, e.g. ARGS="--set gateway.enabled=true"
.PHONY: helm-template
helm-template:
rm -rf bin/chart
helm template sam-mesh ./charts/sam-mesh --output-dir bin/chart $(ARGS)

lint: fmt helm-lint
hack/lint.sh

# fast chart template checks; no cluster needed
.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

.PHONY: verify
verify:
./hack/verify-generated.sh
Expand Down
70 changes: 61 additions & 9 deletions charts/sam-mesh/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# sam-mesh Helm chart

Deploys a self-contained SAM mesh (control plane, router, console, and
optionally an in-cluster Postgres + Dex) for local development, testing, or
self-hosting your own hub.
Deploys a self-contained SAM mesh (control plane, router, console, and an
in-cluster Postgres) for local development, testing, or self-hosting your
own mesh.

> For large-scale production deployments (GKE/EKS/AKS) using externally
> managed Postgres/DNS/OIDC, see the
Expand All @@ -12,9 +12,17 @@ self-hosting your own hub.
## Install

```bash
helm upgrade --install sam-mesh ./charts/sam-mesh --namespace sam --create-namespace
helm upgrade --install sam-mesh ./charts/sam-mesh --namespace sam --create-namespace \
--set controlPlane.oidcIssuer=<your OIDC issuer URL>
```

`controlPlane.oidcIssuer` is required: the chart bundles no identity
provider, and the control plane refuses to start without an issuer. Point it
at your own OIDC provider (Google, Okta, a Dex you run, the cluster's own
issuer for ServiceAccount Workload Identity Federation, …). The `kind` dev
environment (`make kind-up`) deploys its own throwaway Dex from
`development/kind/dex.yaml` and wires it in for you.

At the end of `helm install`/`helm upgrade`, the chart prints the exact
`kubectl` command to retrieve your generated secrets (see below) — read the
NOTES output before doing anything else.
Expand Down Expand Up @@ -59,9 +67,53 @@ administrator to approve each enrollment via `/admin/enrollments` before a
node can join — see the
[Control Plane Configuration guide](https://sam-mesh.dev/docs/user/control-plane-configuration/#6-headless-node-enrollment-bootstrap-token-flow).

## Dex (`dex.enabled`)
## Gateway API (`gateway.enabled`)

Disabled by default. When enabled the chart creates one `Gateway` fronting
the mesh, with one `HTTPRoute`. `gateway.className` is then **required**,
with no default, because the right GatewayClass is provider-specific
(`cloud-provider-kind` in kind, `gke-l7-global-external-managed` on GKE,
`istio`, `envoy-gateway`, …).

The route exposes only the control plane's enrollment surface (`/register`,
`/info`, `/keys`, `/routers/lease`, `/policies`, `/enroll`, `/enroll/status`,
`/refresh`) and the console under `gateway.consolePath`; everything else,
including `/admin` and `/user`, is unrouted. `gateway.adminRoute: true`
additionally routes `/admin` — a dev convenience, leave it off in production.

For the console, the bare prefix (`/console`) is answered with a 302 to
`/console/`, and a `URLRewrite` filter strips the prefix before the request
reaches the console. `URLRewrite` is **Extended** (not core) Gateway API
conformance, so the provider must support it. Set `gateway.consolePath: ""`
to leave the console unrouted.

`listeners`, `hostnames`, `addresses` and `annotations` are passed through to
the Gateway API objects verbatim, so anything the spec allows is expressible.
They default to one plain-HTTP listener on port 80 matching every host, which
suits a local cluster. For example, on GKE:

```yaml
gateway:
enabled: true
className: gke-l7-global-external-managed
listeners:
- name: https
protocol: HTTPS
port: 443
tls:
certificateRefs:
- name: sam-mesh-tls
allowedRoutes:
namespaces:
from: Same
hostnames: [sam.example.com]
addresses:
- type: NamedAddress
value: sam-cp-ip
```

## OIDC login for the console

Disabled by default. The bundled Dex is only meant for local/dev OIDC login
(username/password test users); real deployments should point
`controlPlane.oidcIssuer` at your own identity provider instead of enabling
this.
There is no bundled Dex. Point `controlPlane.oidcIssuer` at your identity
provider and register `https://<control-plane-hostname><consolePath>/auth/callback` as
a redirect URI for the OIDC client the control plane reports.
8 changes: 8 additions & 0 deletions charts/sam-mesh/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,13 @@ valid identity is enrolled automatically without manual admin approval. Set
it to false for production deployments where you want to review enrollments.
{{- end }}

{{- if and .Values.router.hostPort (gt (int .Values.router.replicaCount) 1) }}

WARNING: router.hostPort={{ .Values.router.hostPort }} with replicaCount={{ .Values.router.replicaCount }}.
Each router binds that port on its node, so at most one router runs per node.
With fewer than {{ .Values.router.replicaCount }} schedulable nodes (after router.nodeSelector), the extra
pods stay Pending with "node(s) didn't have free ports".
{{- end }}

See charts/sam-mesh/README.md for more details on configuring OIDC, TLS, and
production values.
30 changes: 26 additions & 4 deletions charts/sam-mesh/templates/bootstrap-job.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@ metadata:
labels:
{{- include "sam-mesh.labels" . | nindent 4 }}
rules:
# resourceNames cannot restrict create (the name is unknown at admission),
# so it gets a bare rule; everything else is pinned to the router-token secret.
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "delete", "patch", "update"]
verbs: ["create"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["{{ include "sam-mesh.fullname" . }}-router-token"]
verbs: ["get", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
Expand Down Expand Up @@ -49,10 +55,27 @@ spec:
spec:
serviceAccountName: {{ include "sam-mesh.fullname" . }}-bootstrap-sa
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
# curlimages/curl's USER is non-numeric; runAsNonRoot needs an explicit uid.
runAsUser: 100
seccompProfile:
type: RuntimeDefault
containers:
- name: bootstrap
image: curlimages/curl:8.6.0
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
env:
- name: NAMESPACE
value: {{ .Release.Namespace | quote }}
Expand Down Expand Up @@ -81,9 +104,8 @@ spec:
{{- $bindings := .Values.bootstrap.bindings -}}
{{- if not $bindings -}}
{{- $defaultRouterSA := printf "user:system:serviceaccount:%s:%s-router-sa" .Release.Namespace (include "sam-mesh.fullname" .) -}}
{{- $bindings = list
{{- $bindings = list
(dict "role" "sam:role:router" "members" (list "group:routers" $defaultRouterSA))
(dict "role" "sam-admin" "members" (list (printf "user:system:serviceaccount:%s:node-a-sa" .Release.Namespace) (printf "user:system:serviceaccount:%s:node-b-sa" .Release.Namespace) (printf "user:system:serviceaccount:%s:node-c-sa" .Release.Namespace) (printf "user:system:serviceaccount:%s:local-node-sa" .Release.Namespace)))
(dict "role" "sam:role:sambox" "members" (list (printf "user:system:serviceaccount:%s:sam-box-sa" .Release.Namespace)))
(dict "role" "sam:role:node" "members" (list "group:data-scientist" "group:users"))
-}}
Expand All @@ -96,7 +118,7 @@ spec:
{"name": "sam-admin", "allowed_services": ["*"], "allowed_targets": ["*"]},
{"name": "sam:role:sambox", "allowed_services": ["*"], "allowed_targets": ["*"]},
{"name": "sam:role:router", "allowed_services": ["*"], "allowed_targets": ["*"]},
{"name": "sam:role:node", "allowed_services": ["mcp://calculator", "mcp://db-agent", "mcp://http-tool", "mcp://stdio-tool", "system://sam.catalog"], "allowed_targets": ["*"]}
{"name": "sam:role:node", "allowed_services": {{ toJson .Values.bootstrap.nodeServices }}, "allowed_targets": ["*"]}
],
"bindings": {{ toJson $bindings }}
}' \
Expand Down
38 changes: 37 additions & 1 deletion charts/sam-mesh/templates/console-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,45 @@ spec:
app: {{ include "sam-mesh.fullname" . }}-console
template:
metadata:
annotations:
# Secrets land as env vars, which are not live-reloaded; roll the pods on change.
checksum/secrets: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }}
labels:
app: {{ include "sam-mesh.fullname" . }}-console
spec:
{{- with .Values.console.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.console.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.console.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.console.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
initContainers:
- name: wait-control-plane
image: curlimages/curl:8.6.0
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
securityContext:
# curlimages/curl's USER is non-numeric; runAsNonRoot needs an explicit uid.
runAsUser: 100
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
command: ["/bin/sh", "-c"]
args:
- |
Expand All @@ -30,8 +62,12 @@ spec:
done
containers:
- name: sam-console
image: "{{ .Values.console.image.repository }}:{{ .Values.global.imageTag }}"
image: "{{ .Values.console.image.repository }}:{{ .Values.console.image.tag | default .Values.global.imageTag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- with .Values.console.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
env:
- name: SAM_ADMIN_TOKEN
valueFrom:
Expand Down
33 changes: 32 additions & 1 deletion charts/sam-mesh/templates/control-plane-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,49 @@ metadata:
{{- include "sam-mesh.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.controlPlane.replicaCount }}
{{- if .Values.controlPlane.hostPort }}
# hostPort: a surged pod can't bind a port the old pod still holds, so the
# default maxSurge=1 deadlocks the rollout on port-saturated nodes.
strategy:
rollingUpdate:
maxSurge: 0
maxUnavailable: 1
{{- end }}
selector:
matchLabels:
app: {{ include "sam-mesh.fullname" . }}-control-plane
template:
metadata:
annotations:
# Secrets land as env vars, which are not live-reloaded; roll the pods on change.
checksum/secrets: {{ include (print $.Template.BasePath "/secrets.yaml") . | sha256sum }}
labels:
app: {{ include "sam-mesh.fullname" . }}-control-plane
spec:
{{- with .Values.controlPlane.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.controlPlane.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.controlPlane.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.controlPlane.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: sam-control-plane
image: "{{ .Values.controlPlane.image.repository }}:{{ .Values.global.imageTag }}"
image: "{{ .Values.controlPlane.image.repository }}:{{ .Values.controlPlane.image.tag | default .Values.global.imageTag }}"
imagePullPolicy: {{ .Values.global.imagePullPolicy }}
{{- with .Values.controlPlane.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
env:
- name: DB_PASSWORD
valueFrom:
Expand Down
20 changes: 20 additions & 0 deletions charts/sam-mesh/templates/db-statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ spec:
labels:
app: {{ include "sam-mesh.fullname" . }}-db
spec:
{{- with .Values.database.postgres.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.database.postgres.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.database.postgres.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: postgres
image: "{{ .Values.database.postgres.image.repository }}:{{ .Values.database.postgres.image.tag }}"
Expand All @@ -34,6 +46,14 @@ spec:
- containerPort: 5432
name: postgres
protocol: TCP
readinessProbe:
exec:
command: ["pg_isready", "-U", {{ .Values.database.postgres.user | quote }}, "-d", {{ .Values.database.postgres.database | quote }}]
periodSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-U", {{ .Values.database.postgres.user | quote }}, "-d", {{ .Values.database.postgres.database | quote }}]
periodSeconds: 15
resources:
{{- toYaml .Values.database.postgres.resources | nindent 10 }}
volumeMounts:
Expand Down
Loading
Loading