A rolling upgrade needs two substrate versions running in one cluster, split by node: `atelet` and `ateom` speak a node-local protocol with no cross-version guarantees, so everything on a node has to come from one build. Today nothing records which build a node runs, and the `atelet` DaemonSet is a single fixed-name object, so a second version can only replace the first in place, instead of our porposed node by node rolling ([design](https://docs.google.com/document/d/1JduAyGZFyqdNp4UhKiv0EN-is3iW5tf5BwGTWbt2ouI/edit?usp=sharing&resourcekey=0-MXG12QCkleIxhY7cOB7g6A)). This PR is the basis for upgrade. Nodes and the `atelet` DaemonSet get keyed by an `ate.dev/substrate-version` label whose value is the build version stamped into the binaries. ### One derivation for the version label `internal/versionlabel` turns the build version into its two forms in one place: | form | grammar | used for | |---|---|---| | label value | k8s label value | node labels, DaemonSet labels, nodeSelectors | | name suffix | DNS-1123 | the DaemonSet name `atelet-<suffix>` | A version that is not a valid label value is rejected, because the label has to match what the `ldflags` stamp put into the binaries through MAKEFILE and `ko`. It's also exposed shell, so the install script can use it. ### A DaemonSet per version `atelet-<suffix>` as the name, the version label on metadata, selector, and pod template, and a pod nodeSelector on the same label. Two versions run side by side on disjoint old/new node sets. ### The install becomes version-aware - `install-ate.sh` reads the version from the same `make ldflags` output it stamps the binaries with, then fills the manifest placeholders. - It labels the nodes that exist at install time, nodes come after it carry no version label yet (so add notes in README). Re-running the install is idempotent, this is the basis for upgrade runbook later. - The `README` documents the invariant, how to read the installed version off the DaemonSet, and that a node added later hosts no workers until it carries the label. On GKE the node pool label is the birth default for new nodes - added in `tools/setup-gcp/README.md`. ### Follow-ups - `ate-setup` need same modification - minimize the affect for daily developer (pin the `VERSION` as `{USER}-dev` instead of from git - the manual rolling-upgrade runbook. Fix #1270 Ref [design doc](https://docs.google.com/document/d/1JduAyGZFyqdNp4UhKiv0EN-is3iW5tf5BwGTWbt2ouI/edit?usp=sharing&resourcekey=0-MXG12QCkleIxhY7cOB7g6A)
GCP Setup Tool (setup-gcp)
This tool automates the provisioning and configuration of Google Cloud Platform (GCP) resources required to run Agent Substrate. It is designed to be idempotent, meaning it can be run multiple times safely to ensure the environment is correctly configured.
Overview
The setup-gcp tool provisions and configures GCP resources for Agent Substrate.
It uses a hierarchical command structure:
setup-gcp(root)enable apis- Enable required GCP APIs.create- Parent command for resource creation.cluster- Create GKE cluster.bucket- Create GCS bucket.iam- Create IAM policy bindings and grant permissions.dashboards- Create Cloud Monitoring dashboards.
bootstrap- Run all setup steps in order.
Prerequisites
- Go: Ensure Go is installed (version compatible with the project, see root
go.mod). - Google Cloud SDK (
gcloud): Installed and authenticated. - Application Default Credentials (ADC): The tool uses Google Cloud client libraries. You must set up ADC:
gcloud auth application-default login - Target Project: You must have a GCP project created and have sufficient permissions (typically Owner or Editor) to create resources like GKE clusters, GCS buckets, and IAM bindings.
Configuration & Defaults
All CLI flags can be configured via environment variables. If an environment variable is set, it will be used as the default value for the corresponding flag. Command-line flags always take precedence over environment variables.
A convenient way to manage these is to copy the example environment file, customize it, and source it before running the tool:
cp hack/ate-dev-env.sh.example .ate-dev-env.sh
# Edit .ate-dev-env.sh to match your project and preferences
source .ate-dev-env.sh
Global Flags
These flags can be passed to the root command and apply to all subcommands:
| Flag | Description | Default Env Var | Fallback Default |
|---|---|---|---|
--project-id |
GCP Project ID. | PROJECT_ID |
None |
--project-number |
GCP Project Number (required for IAM). | PROJECT_NUMBER |
None |
--region |
GCP Region for regional resources. | GCE_REGION |
us-west1 |
Subcommands
1. Enable APIs
Enables the required GCP APIs for the project.
go run ./tools/setup-gcp enable apis [flags]
Flags:
- (Uses global
--project-id)
2. Create Cluster
Creates a GKE cluster configured for Agent Substrate.
Warning
Agent Substrate requires two Kubernetes beta APIs —
certificates.k8s.io/v1beta1/podcertificaterequestsandcertificates.k8s.io/v1beta1/clustertrustbundles— and GKE only honorsenableK8sBetaApisat cluster creation. Enabling them later on an existing cluster is not recoverable in place: the tool's reconcile path issues the update, but the APIs do not become served — the cluster must be recreated with them enabled.create clustersets them for you; if you bring your own cluster, create it with both APIs enabled from the start, e.g.:gcloud container clusters create "${CLUSTER_NAME}" ... \ --enable-kubernetes-unstable-apis=certificates.k8s.io/v1beta1/podcertificaterequests,certificates.k8s.io/v1beta1/clustertrustbundlesThe symptom of a cluster without them: the install hangs at "Waiting for podcertificate ClusterTrustBundles to be ready" and
kubectl get clustertrustbundlesreports the resource type is not served.
go run ./tools/setup-gcp create cluster [flags]
Flags:
| Flag | Description | Default Env Var | Fallback Default |
|---|---|---|---|
--name |
Name of the GKE cluster. | CLUSTER_NAME |
substrate-poc |
--location |
Zone or region for the cluster. | CLUSTER_LOCATION |
us-west1-c |
--version |
Kubernetes version. | CLUSTER_VERSION |
None |
--network |
VPC network name. | NETWORK |
default |
--subnetwork |
VPC subnetwork name. | SUBNETWORK |
default |
--machine-type |
Machine type for the gVisor node pool. | GVISOR_NODE_MACHINE_TYPE |
c3-standard-4 |
Node version labels: pool labels are the birth default for every node GKE
creates later (autoscaling, auto-repair, node upgrades), and setup-gcp does
not set ate.dev/substrate-version on the pool, so those nodes arrive
unlabeled and run no dataplane pods (see the README's note on node version
labels). Stamp the pool with
gcloud container node-pools update ... --node-labels=... (list the existing
labels first and carry them all over; the flag replaces the full set), and
create additional pools with
--node-labels=ate.dev/substrate-version=<build version>.
3. Create Bucket
Creates a GCS bucket for storing snapshots.
go run ./tools/setup-gcp create bucket [flags]
Flags:
| Flag | Description | Default Env Var | Fallback Default |
|---|---|---|---|
--name |
Name of the GCS bucket. | BUCKET_NAME |
None (Required*) |
*Note: Required unless the BUCKET_NAME environment variable is set.
4. Create IAM
Configures IAM permissions and Workload Identity bindings.
go run ./tools/setup-gcp create iam [flags]
Flags:
| Flag | Description | Default Env Var | Fallback Default |
|---|---|---|---|
--bucket |
GCS bucket name. | BUCKET_NAME |
None (Required for bucket bindings*) |
--gke-nodes |
Grant GKE nodes permission to pull images. | - | true |
--atelet |
Grant atelet project-level permissions. | - | true |
--bucket-bindings |
Grant atelet access to the snapshot bucket. | - | true |
*Note: Required for bucket bindings unless the BUCKET_NAME environment variable is set.
What create iam actually grants
On a fresh GCP project these bindings do not exist, and nothing else creates
them — without them atelet cannot read or write snapshots (403 from GCS on
the first suspend/resume) and nodes cannot pull images. bootstrap and
create iam apply them for you; the table below is the reference for auditing
them, or for applying them by hand in projects where you cannot run the tool
with project-level IAM permissions.
atelet authenticates via GKE Workload Identity
Federation:
its Kubernetes ServiceAccount (ate-system/atelet, created by
install-ate.sh) is addressed directly as an IAM principal — no Google
service account is created or impersonated. This only works on a cluster with
Workload Identity enabled (create cluster enables the
PROJECT_ID.svc.id.goog pool; a pre-existing cluster must have it enabled
too, or atelet's GCS calls fail with 401).
| Member | Role | Resource | Why |
|---|---|---|---|
| atelet WI principal¹ | roles/storage.objectAdmin |
project² | Read/write actor snapshots in GCS |
| atelet WI principal¹ | roles/artifactregistry.reader |
project² | Pull sandbox runtime assets |
| atelet WI principal¹ | roles/storage.objectAdmin |
snapshot bucket | Read/write snapshot objects |
| atelet WI principal¹ | roles/storage.bucketViewer |
snapshot bucket | List/stat the snapshot bucket |
| default compute SA³ | roles/storage.objectViewer |
project² | Nodes pull images from GCS-backed registries |
| default compute SA³ | roles/artifactregistry.reader |
project² | Nodes pull images from Artifact Registry |
¹ principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/PROJECT_ID.svc.id.goog/subject/ns/ate-system/sa/atelet — note it is keyed to the project number, not the ID.
² Project-level today; scoping these down is tracked in the TODOs in cmd/iam.go.
³ PROJECT_NUMBER-compute@developer.gserviceaccount.com (least-privileged node SA is #76).
Manual equivalent:
ATELET="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/ate-system/sa/atelet"
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="${ATELET}" --role=roles/storage.objectAdmin
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="${ATELET}" --role=roles/artifactregistry.reader
gcloud storage buckets add-iam-policy-binding "gs://${BUCKET_NAME}" \
--member="${ATELET}" --role=roles/storage.objectAdmin
gcloud storage buckets add-iam-policy-binding "gs://${BUCKET_NAME}" \
--member="${ATELET}" --role=roles/storage.bucketViewer
5. Create Dashboards
Creates or updates Cloud Monitoring dashboards.
go run ./tools/setup-gcp create dashboards [flags]
Flags:
| Flag | Description | Default Env Var | Fallback Default |
|---|---|---|---|
--dir |
Directory containing dashboard JSON files. | DASHBOARD_DIR |
tools/setup-gcp/dashboards |
6. Bootstrap (All Steps)
Runs all the setup steps in the correct order to fully bootstrap the environment.
go run ./tools/setup-gcp bootstrap [flags]
Flags:
| Flag | Description | Default Env Var | Fallback Default |
|---|---|---|---|
--cluster-name |
Name of the GKE cluster. | CLUSTER_NAME |
substrate-poc |
--cluster-location |
Zone or region for the cluster. | CLUSTER_LOCATION |
us-west1-c |
--cluster-version |
Kubernetes version. | CLUSTER_VERSION |
None |
--network |
VPC network name. | NETWORK |
default |
--subnetwork |
VPC subnetwork name. | SUBNETWORK |
default |
--machine-type |
Machine type for the gVisor node pool. | GVISOR_NODE_MACHINE_TYPE |
c3-standard-4 |
--bucket-name |
Name of the GCS bucket for snapshots. | BUCKET_NAME |
None (Required*) |
--dashboard-dir |
Directory containing dashboard JSON files. | DASHBOARD_DIR |
tools/setup-gcp/dashboards |
*Note: Required unless the BUCKET_NAME environment variable is set.
Examples
Run the tool from the repository root to ensure relative paths to dashboard configurations are resolved correctly.
Bootstrap everything using environment variables
If you have sourced your .ate-dev-env.sh:
go run ./tools/setup-gcp bootstrap
Bootstrap everything overriding some values
go run ./tools/setup-gcp bootstrap \
--cluster-name="custom-cluster" \
--machine-type="n2-standard-8"
Only create the cluster (using env vars for defaults)
go run ./tools/setup-gcp create cluster