# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

zrok-orchestration is a Go REST API that manages AWS infrastructure provisioning for the Zrok networking platform. It uses Pulumi Automation API to programmatically create, update, delete, and monitor cloud stacks. The service runs as a containerized ECS task and orchestrates resources like load balancers, ECS services, ACM certificates, Route53 DNS, and Secrets Manager entries.

## Build & Run Commands

```bash
go build -o zrok-orchestration .    # Build binary
go run main.go                       # Run locally (requires S3_BUCKET_ROOT env var)
go mod download                      # Download dependencies
```

No linting is configured. The only tests are in `templates/frontdoor-tcp-shares/` (`naming_test.go`, `taskDefintion_test.go`); run them with `cd templates/frontdoor-tcp-shares && go test ./...`.

## Required Environment Variables

- `S3_BUCKET_ROOT` - Required for app to launch (Pulumi S3 backend)
- `ENVIRONMENT` - staging/production (default: staging)
- `SECRETSPROVIDER` - Pulumi secrets provider, defaults to AWS KMS alias
- `CNAME_DOMAINNAME` - Custom domain CNAME target (default: domains.staging.zrok.io)

See `config/config.go` for all config with defaults.

## Architecture

### Request Flow

```
HTTP Request → Gin Router (main.go) → Controller Handler → Pulumi Automation API → AWS
```

Each API call creates a temporary workspace directory, loads a Pulumi template, and executes stack operations (up/refresh/destroy). State is stored in S3 (`s3://nf-pulumi-state`), not locally.

### Code Layout

- **`main.go`** - Entry point: loads config, sets up Gin router and routes
- **`config/`** - Environment-variable-driven configuration via `go-envconfig`
- **`controllers/`** - HTTP handlers and Pulumi orchestration logic
  - `controller.go` - Controller struct definition
  - `common.go` - Shared Pulumi workspace/stack helpers (Initialize, getWorkspace, createProject, selectStack, refresh operations)
  - `stacks.go` - Custom domain stack CRUD handlers (`/stacks` endpoints)
  - `frontdoor-tcpshares.go` - TCP share stack CRUD handlers (`/tcp-share` endpoints, `/frontdoor-tcp-share` deprecated aliases)
- **`middleware/`** - Zerolog structured JSON logging with B3 distributed tracing headers
- **`templates/`** - Pulumi Go programs used as infrastructure templates
  - `zrok-custom-domains/` - ACM certs, Route53, ECS, ALB, Secrets Manager, CloudWatch
  - `frontdoor-tcp-shares/` - Load balancers, ECS tasks, CloudWatch for TCP shares

### Two Stack Types

1. **Custom Domain Stacks** (`/stacks`) - Full infrastructure for Zrok custom domains: certificates, DNS, ECS services, load balancers. Success = 15+ resources and ISSUED certificate status.
2. **TCP Share Stacks** (`/tcp-share`) - Infrastructure for one TCP share: an NLB listener, a target group, a log group, a task definition and an ECS service running `frontdoor-mtls`. Success = 10+ resources.

### TCP shares come in two shapes

A share is either zrok-backed or ziti-backed, and a stack keeps the shape it was created with. The create request names one or the other, never both; `validateShareShape` rejects a body that names neither.

| | zrok share | ziti share |
|---|---|---|
| Request names | `share_token` + `account_token` | `service_name` + `ziti_identity_arn` |
| Container reaches the share by | a zrok share token, through the zrok control plane | dialing a ziti service directly |
| Image parameter | `/<env>/container-image/frontdoor-mtls` | `/<env>/container-image/frontdoor-mtls-ziti` |
| Task definition template | `zrok-orchestration-task-definition.json` | `frontdoor-mtls-ziti-task-definition.json` |

**The two images are not interchangeable, which is why they have separate parameters.** The ziti entrypoint exits when `FRONTDOOR_SERVICE_NAME` is absent. One parameter for both would hand a refreshed zrok stack an image that cannot start, and the ECS deployment circuit breaker would roll it back with nothing in the logs naming the cause.

**Every credential arrives as an ARN, never as material.** The caller writes certificates to SSM Parameter Store and the ziti identity to Secrets Manager, both in the customer account, and passes the ARN. The identity goes to Secrets Manager because an enrolled identity document is around 10 KB and an SSM parameter value stops at 8 KB. The stack sets every ARN with `Secret: false` and renders it into the ECS `secrets` block as a `valueFrom`, so nothing secret enters Pulumi state. ECS resolves both kinds of ARN through that same field, so the two stores differ only in the IAM statement that grants the read. `zrok-custom-domains` takes a ziti identity as a Pulumi secret and writes it to AWS itself — do not copy that arrangement here.

**`client_cert_revision` exists to force a redeployment.** A bundle rotation leaves the parameter ARN alone, so without it the rendered task definition is identical and Pulumi finds no diff to apply.

`PUT /tcp-share` writes back only the fields the body carries and then runs the stack up. It is how a renamed ziti service, a rotated certificate bundle and a moved ingress port reach a live share. The request fields are pointers, so an omitted field keeps the value the stack holds and a field sent as `""` clears it — that is how a compromised client certificate or a stale allow-list is revoked. `service_name`, `ziti_identity_arn`, `server_cert_arn` and `server_key_arn` reject an empty value, because a share cannot run without them.

`taskDefintion_test.go` renders both templates and parses the result. The `secrets` block is encoded with `encoding/json` and every caller-supplied value is JSON-escaped before it reaches a template, so a quote in a service name or an absent field cannot produce a task definition ECS refuses. The test holds both properties, because that failure belongs in a test and not in a deploy.

### Key Patterns

- **Temporary workspaces**: Each Pulumi operation creates a temp dir, initializes from a template, runs the operation, then the dir is disposable. State lives in S3.
- **Stack locking**: Operations detect Pulumi lock conflicts via error message parsing and report LOCKED status.
- **Resource prefix**: Optional `RESOURCE_PREFIX` env var enables multi-tenant resource naming.
- **Version support**: `ZROK_BASE_VERSION` controls v1 vs v2 Zrok infrastructure variants.

### Infrastructure

- **Pulumi templates** are standalone Go programs in `templates/` with their own `go.mod`
- **`pulumi/`** directory contains a separate Pulumi program for shared infrastructure (security groups, Global Accelerator, WAF) across multiple AWS accounts
- **`terraform/`** defines IAM policies, security groups, ECS service, and SSM parameters with S3 backend state

### CI/CD

Jenkins pipelines in `jenkinsfiles/` handle build (Go build → Docker → ECR push) and deployment. Docker image is pushed to `447513199460.dkr.ecr.us-east-1.amazonaws.com/zrok-orchestration`. Source is on Bitbucket at `netfoundry/zrok-orchestration`.

## Key Dependencies

- **Gin** (`gin-gonic/gin`) - HTTP framework
- **Pulumi SDK v3** (`pulumi/pulumi/sdk/v3`) - Infrastructure as Code automation
- **Zerolog** (`rs/zerolog`) - Structured JSON logging
- **go-envconfig** (`sethvargo/go-envconfig`) - Environment variable configuration