Files
LibreMail-Bug-Report-Ingest/infra
JMR-devandClaude Opus 4.8 21665b172d #2 Pulumi IaC scaffolding: Worker, R2 bucket, Google Cloud DNS
Add an infra/ Pulumi (Go) program in its own module
(github.com/JMR-dev/LibreMail-Bug-Report-Ingest/infra) that declares the
three pieces of edge/DNS infrastructure for the bug-report ingest pipeline:

- Cloudflare Worker script (libremail-bug-report-ingest, built in #1)
- Cloudflare R2 bucket (libremail-bug-reports) for encrypted reports (ADR 0001)
- Google Cloud DNS record (CNAME) pointing the ingest hostname at the Worker,
  referencing an existing managed zone by name

Per-environment stacks (dev/prod) via Pulumi.<stack>.yaml + pulumi.Config;
account id, zone, domain, etc. are parameterized through config and secrets
are kept out of git (documented in infra/README.md). Worker content is a
documented placeholder because the real TinyGo->Wasm artifact is produced by
the build pipeline.

Mock-based unit tests (pulumi.RunErr + pulumi.WithMocks) assert the registered
resources and their inputs; go build + go vet + go test all pass without the
Pulumi CLI. Structured so the #7 Cloudflare Rate Limiting ruleset can be added
later (reserved cloudflareZoneId config + insertion point in deploy.go).

Providers: pulumi-cloudflare v6.17.0, pulumi-gcp v8.41.1, pulumi/sdk v3.250.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:45:07 -05:00
..

Infrastructure (Pulumi, Go)

Infrastructure-as-code for the LibreMail bug-report ingest pipeline. This is its own Go module (github.com/JMR-dev/LibreMail-Bug-Report-Ingest/infra) so the Worker module at the repo root stays lean.

It provisions, with the Pulumi Go SDK and the pulumi-cloudflare and pulumi-gcp providers:

Resource Type Purpose
ingest-worker cloudflare.WorkersScript The ingest Worker (libremail-bug-report-ingest, built in #1).
reports-bucket cloudflare.R2Bucket Encrypted bug-report storage (libremail-bug-reports). Per ADR 0001 only ciphertext is written.
ingest-dns-record gcp.dns.RecordSet Google Cloud DNS record pointing the ingest hostname at the Worker's route/custom domain.

DNS authority is Google Cloud DNS. The managed zone is referenced by name (it already exists / is managed elsewhere), and this stack only adds a record to it.

Worker content is a placeholder. The deployed Worker is Go compiled by TinyGo to Wasm plus the syumai/workers ES-module shim, emitted by pnpm run build into ../build/ (git-ignored, produced by CI). This program ships a documented placeholder module body so the resource is fully described and unit-testable without the artifact. Wire the real artifact at deploy time via the workerScriptContent config, or by setting ContentFile / ContentSha256 on the Worker resource to ../build/worker.mjs in the deploy pipeline.

Prerequisites

  • Go (matching the repo toolchain) — go build ./... / go test ./... work without the Pulumi CLI.
  • The Pulumi CLI — required only to pulumi preview / pulumi up. It is not installed in CI yet, so a real pulumi preview is a follow-up (see "Deploying").

Configuration

Config is per-stack (Pulumi.<stack>.yaml). Program keys are namespaced by the project name, libremail-bug-report-ingest-infra. Provider keys use the cloudflare: / gcp: namespaces.

Program config (namespace libremail-bug-report-ingest-infra)

Key Required Default Description
cloudflareAccountId yes — Cloudflare account that owns the Worker + R2 bucket.
dnsManagedZone yes — Name of the existing Google Cloud DNS managed zone.
dnsRecordName yes — Ingest hostname as an FQDN with a trailing dot (e.g. bugreport.libremail.example.).
dnsRecordTarget yes — CNAME target = the Worker route/custom domain (FQDN, trailing dot).
workerName no libremail-bug-report-ingest Worker script name (matches wrangler.jsonc).
workerCompatibilityDate no 2025-06-01 Worker runtime compatibility date.
workerScriptContent no placeholder Override the Worker module body (normally supplied by the build pipeline).
r2BucketName no libremail-bug-reports R2 bucket name.
r2BucketLocation no (provider default) R2 location hint: apac, eeur, enam, weur, wnam, oc.
dnsRecordType no CNAME DNS record type.
dnsTtlSeconds no 300 DNS record TTL (seconds).
gcpProject no (from gcp:project) Project the record belongs to, if different from the provider project.
cloudflareZoneId no — Reserved for #7 (rate-limit ruleset) and Worker routes/custom domain. Unused today.

Set a non-secret value with, e.g.:

pulumi config set libremail-bug-report-ingest-infra:cloudflareAccountId <account-id>

Secrets and credentials — DO NOT COMMIT

Set these as encrypted Pulumi secrets (--secret) or via the provider's environment variables. Never place plaintext secrets in Pulumi.<stack>.yaml.

What How Notes
Cloudflare API token pulumi config set --secret cloudflare:apiToken <token> (or env CLOUDFLARE_API_TOKEN) Scope it to Workers Scripts + R2 admin for the account.
GCP project pulumi config set gcp:project <project-id> Not secret; identifies the GCP project.
GCP credentials pulumi config set --secret gcp:credentials "$(cat key.json)" (or env GOOGLE_CREDENTIALS / Application Default Credentials) Service account with Cloud DNS admin on the zone.

The bug-report encryption keyring (BUGREPORT_ENC_KEYRING, see ADR 0001) lives in Cloudflare Secrets Store, bound to the Worker — it is managed there, not committed here.

Deploying

pulumi stack select dev            # or: pulumi stack init dev
# set the REPLACE_ME_* config values + secrets above, then:
pulumi preview
pulumi up

Deployment is gated behind a maintainer check-in and the Pulumi CLI is not yet available in this environment, so this change ships compile- and test-verified only. Running a real pulumi preview against the accounts is a follow-up.

Testing (no Pulumi CLI required)

The program is exercised with the Pulumi Go SDK's mocking (pulumi.RunErr + pulumi.WithMocks), which registers resources against an in-memory monitor — no cloud calls, no CLI. The tests assert that the expected resources are registered with the expected inputs (Worker name/account, R2 bucket name/location, DNS type/name/target/ttl).

go vet ./...
go build ./...
go test ./...

Adding rate limiting later (#7)

The abuse/rate-limit ADR chose to implement ingest rate limiting as Cloudflare Rate Limiting rules via Pulumi (cloudflare.NewRuleset, phase http_ratelimit, scoped to a zone). That is out of scope for this ticket. The cloudflareZoneId config key and the structure of deploy.go leave a clean insertion point; #7 adds the ruleset resource there.