From 2b8f6f5b3bc1f2586facded4c32ad9dc00b84448 Mon Sep 17 00:00:00 2001 From: Ludovic Vielle Date: Wed, 24 Jun 2026 11:06:49 +0200 Subject: [PATCH] Add Secrets Manager resolution to probod-bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a Resolver that owns env lookup and typed parsing for probod-bootstrap. Env values prefixed with aws:// are fetched from AWS Secrets Manager (plaintext SecretString); each secret ID is cached per run. Builder now takes a Resolver only. Prefix every probod-bootstrap input with PROBOD_ so bootstrap config does not collide with unrelated process environment (for example AWS_* used by other tooling). Secrets Manager authentication uses the standard AWS SDK default chain (AWS_REGION, IAM role, profile); PROBOD_AWS_* vars configure S3 in the generated config only. Update Helm deployment env names, GNUmakefile dev-config, Lima provision, e2e testutil, compose.prod.yaml, and docs. Deployments must rename bootstrap env vars to PROBOD_* (e.g. AUTH_COOKIE_SECRET → PROBOD_AUTH_COOKIE_SECRET). BREAKING CHANGE: all env vars are now prefixed by `PROBOD_`. Signed-off-by: Ludovic Vielle --- .env.example | 201 ++++---- .github/workflows/make.yaml | 2 +- GNUmakefile | 25 +- cmd/probod-bootstrap/main.go | 2 +- compose.prod.yaml | 46 +- contrib/claude/config.md | 5 +- contrib/claude/e2e.md | 20 +- contrib/helm/README.md | 35 ++ .../charts/probo/templates/deployment.yaml | 232 ++++----- contrib/lima/provision.sh | 28 +- e2e/internal/testutil/testutil.go | 64 +-- entrypoint.sh | 1 + go.mod | 7 +- go.sum | 14 +- pkg/bootstrap/builder.go | 464 ++++++++---------- pkg/bootstrap/builder_test.go | 334 ++++++------- pkg/bootstrap/resolver.go | 225 +++++++++ pkg/bootstrap/resolver_test.go | 179 +++++++ 18 files changed, 1152 insertions(+), 732 deletions(-) create mode 100644 pkg/bootstrap/resolver.go create mode 100644 pkg/bootstrap/resolver_test.go diff --git a/.env.example b/.env.example index 31ce84477..71707a4be 100644 --- a/.env.example +++ b/.env.example @@ -11,127 +11,136 @@ # - `#` starts a comment, `export KEY=value` is accepted. # - Malformed assignments (e.g. unquoted spaces) abort `make dev-config`. # -# See pkg/bootstrap/builder.go for the full env-var surface; the values +# All probod-bootstrap inputs are prefixed with PROBOD_. See pkg/bootstrap/builder.go; the values # set below in the Makefile's `cfg/dev.yaml` recipe are the dev defaults # that this file can override. # ── Base URL & ports ────────────────────────────────────────────────── # PROBOD_BASE_URL=http://localhost:8080 -# API_ADDR=:8080 -# API_CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173,http://localhost:5174 -# TRUST_CENTER_HTTP_ADDR=:80 -# TRUST_CENTER_HTTPS_ADDR=:443 +# PROBOD_API_ADDR=:8080 +# PROBOD_API_CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173,http://localhost:5174 +# PROBOD_TRUST_CENTER_HTTP_ADDR=:80 +# PROBOD_TRUST_CENTER_HTTPS_ADDR=:443 # ── Observability ───────────────────────────────────────────────────── -# METRICS_ADDR=localhost:8081 -# TRACING_ADDR=localhost:4317 +# PROBOD_METRICS_ADDR=localhost:8081 +# PROBOD_TRACING_ADDR=localhost:4317 # ── Secrets (rotate before prod) ────────────────────────────────────── # PROBOD_ENCRYPTION_KEY=thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA= -# AUTH_COOKIE_SECRET=this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes -# AUTH_PASSWORD_PEPPER=this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes +# PROBOD_AUTH_COOKIE_SECRET=this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes +# PROBOD_AUTH_PASSWORD_PEPPER=this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes +# +# Optional: load secrets from AWS Secrets Manager by setting env vars to +# aws:// (e.g. PROBOD_ENCRYPTION_KEY=aws://probo/sandbox/probod/encryption_key). +# The path after aws:// is the Secrets Manager secret name or ARN; the +# plaintext SecretString is used directly. Plain env values are also supported. +# Requires AWS credentials for Secrets Manager resolution (IAM role, AWS_ACCESS_KEY_ID, +# or AWS_PROFILE) and AWS_REGION. These use the standard AWS SDK default chain, not +# PROBOD_AWS_*. +# PROBOD_ENCRYPTION_KEY=aws://probo/sandbox/probod/encryption_key # ── Cookie ──────────────────────────────────────────────────────────── -# AUTH_COOKIE_DOMAIN=localhost -# AUTH_COOKIE_SECURE=false -# AUTH_COOKIE_DURATION=24 +# PROBOD_AUTH_COOKIE_DOMAIN=localhost +# PROBOD_AUTH_COOKIE_SECURE=false +# PROBOD_AUTH_COOKIE_DURATION=24 # ── Postgres ────────────────────────────────────────────────────────── -# PG_ADDR=localhost:5432 -# PG_USERNAME=postgres -# PG_PASSWORD=postgres -# PG_DATABASE=probod -# PG_POOL_SIZE=100 -# PG_MIN_POOL_SIZE=10 -# PG_MAX_CONN_IDLE_TIME_SECONDS=1800 -# PG_MAX_CONN_LIFETIME_SECONDS=3600 -# PG_DEBUG=false +# PROBOD_PG_ADDR=localhost:5432 +# PROBOD_PG_USERNAME=postgres +# PROBOD_PG_PASSWORD=postgres +# PROBOD_PG_DATABASE=probod +# PROBOD_PG_POOL_SIZE=100 +# PROBOD_PG_MIN_POOL_SIZE=10 +# PROBOD_PG_MAX_CONN_IDLE_TIME_SECONDS=1800 +# PROBOD_PG_MAX_CONN_LIFETIME_SECONDS=3600 +# PROBOD_PG_DEBUG=false # ── Object storage (SeaweedFS via compose) ──────────────────────────── -# AWS_REGION=us-east-1 -# AWS_BUCKET=probod -# AWS_ACCESS_KEY_ID=probod -# AWS_SECRET_ACCESS_KEY=thisisnotasecret -# AWS_ENDPOINT=http://127.0.0.1:8333 +# PROBOD_AWS_REGION=us-east-1 +# PROBOD_AWS_BUCKET=probod +# PROBOD_AWS_ACCESS_KEY_ID=probod +# PROBOD_AWS_SECRET_ACCESS_KEY=thisisnotasecret +# PROBOD_AWS_ENDPOINT=http://127.0.0.1:8333 # ── Mailer (Mailpit via compose) ────────────────────────────────────── -# SMTP_ADDR=localhost:1025 -# SMTP_USER= -# SMTP_PASSWORD= -# SMTP_TLS_REQUIRED=false -# MAILER_SENDER_EMAIL=no-reply@notification.getprobo.com -# MAILER_SENDER_NAME=Probo +# PROBOD_SMTP_ADDR=localhost:1025 +# PROBOD_SMTP_USER= +# PROBOD_SMTP_PASSWORD= +# PROBOD_SMTP_TLS_REQUIRED=false +# PROBOD_MAILER_SENDER_EMAIL=no-reply@notification.getprobo.com +# PROBOD_MAILER_SENDER_NAME=Probo # ── LLM providers ───────────────────────────────────────────────────── -# OPENAI_API_KEY=sk-... -# ANTHROPIC_API_KEY=sk-ant-... -# LLM_DEFAULT_PROVIDER=openai -# LLM_DEFAULT_MODEL_NAME=gpt-4o +# PROBOD_OPENAI_API_KEY=sk-... +# PROBOD_ANTHROPIC_API_KEY=sk-ant-... +# PROBOD_LLM_DEFAULT_PROVIDER=openai +# PROBOD_LLM_DEFAULT_MODEL_NAME=gpt-4o # ── LLM-backed workers (inherit from LLM defaults when unset) ───────── -# PROBO_AGENT_PROVIDER=anthropic -# PROBO_AGENT_MODEL_NAME=claude-sonnet-4-6 -# EVIDENCE_DESCRIBER_PROVIDER=openai -# EVIDENCE_DESCRIBER_MODEL_NAME=gpt-4o-mini -# AGENT_THIRD_PARTY_VETTER_PROVIDER=openai # inherits AGENT_DEFAULT_PROVIDER when unset -# AGENT_THIRD_PARTY_VETTER_MODEL_NAME=gpt-4o # inherits AGENT_DEFAULT_MODEL_NAME when unset -# THIRD_PARTY_VETTING_INTERVAL=10 -# THIRD_PARTY_VETTING_STALE_AFTER=1500 -# THIRD_PARTY_VETTING_MAX_CONCURRENCY=1 +# PROBOD_PROBO_AGENT_PROVIDER=anthropic +# PROBOD_PROBO_AGENT_MODEL_NAME=claude-sonnet-4-6 +# PROBOD_EVIDENCE_DESCRIBER_PROVIDER=openai +# PROBOD_EVIDENCE_DESCRIBER_MODEL_NAME=gpt-4o-mini +# PROBOD_AGENT_THIRD_PARTY_VETTER_PROVIDER=openai # inherits AGENT_DEFAULT_PROVIDER when unset +# PROBOD_AGENT_THIRD_PARTY_VETTER_MODEL_NAME=gpt-4o # inherits AGENT_DEFAULT_MODEL_NAME when unset +# PROBOD_THIRD_PARTY_VETTING_INTERVAL=10 +# PROBOD_THIRD_PARTY_VETTING_STALE_AFTER=1500 +# PROBOD_THIRD_PARTY_VETTING_MAX_CONCURRENCY=1 # ── Document signature/approval notifications (seconds) ─────────────── -# DOCUMENT_NOTIFICATION_INTERVAL=300 -# DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY=900 -# DOCUMENT_NOTIFICATION_REMINDER_INTERVAL=86400 +# PROBOD_DOCUMENT_NOTIFICATION_INTERVAL=300 +# PROBOD_DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY=900 +# PROBOD_DOCUMENT_NOTIFICATION_REMINDER_INTERVAL=86400 # ── OIDC sign-in providers ──────────────────────────────────────────── -# AUTH_GOOGLE_CLIENT_ID= -# AUTH_GOOGLE_CLIENT_SECRET= -# AUTH_MICROSOFT_CLIENT_ID= -# AUTH_MICROSOFT_CLIENT_SECRET= +# PROBOD_AUTH_GOOGLE_CLIENT_ID= +# PROBOD_AUTH_GOOGLE_CLIENT_SECRET= +# PROBOD_AUTH_MICROSOFT_CLIENT_ID= +# PROBOD_AUTH_MICROSOFT_CLIENT_SECRET= # ── Connectors (set _CLIENT_ID to enable; _CLIENT_SECRET is required) ─ -# CONNECTOR_SLACK_CLIENT_ID= -# CONNECTOR_SLACK_CLIENT_SECRET= -# CONNECTOR_SLACK_SIGNING_SECRET= -# CONNECTOR_HUBSPOT_CLIENT_ID= -# CONNECTOR_HUBSPOT_CLIENT_SECRET= -# CONNECTOR_GITHUB_CLIENT_ID= -# CONNECTOR_GITHUB_CLIENT_SECRET= -# CONNECTOR_NOTION_CLIENT_ID= -# CONNECTOR_NOTION_CLIENT_SECRET= -# CONNECTOR_SENTRY_CLIENT_ID= -# CONNECTOR_SENTRY_CLIENT_SECRET= -# CONNECTOR_INTERCOM_CLIENT_ID= -# CONNECTOR_INTERCOM_CLIENT_SECRET= -# CONNECTOR_DOCUSIGN_CLIENT_ID= -# CONNECTOR_DOCUSIGN_CLIENT_SECRET= -# CONNECTOR_BREX_CLIENT_ID= -# CONNECTOR_BREX_CLIENT_SECRET= -# CONNECTOR_GITLAB_CLIENT_ID= -# CONNECTOR_GITLAB_CLIENT_SECRET= -# CONNECTOR_BITBUCKET_CLIENT_ID= -# CONNECTOR_BITBUCKET_CLIENT_SECRET= -# CONNECTOR_HEROKU_CLIENT_ID= -# CONNECTOR_HEROKU_CLIENT_SECRET= -# CONNECTOR_PAGERDUTY_CLIENT_ID= -# CONNECTOR_PAGERDUTY_CLIENT_SECRET= -# CONNECTOR_ASANA_CLIENT_ID= -# CONNECTOR_ASANA_CLIENT_SECRET= -# CONNECTOR_NETLIFY_CLIENT_ID= -# CONNECTOR_NETLIFY_CLIENT_SECRET= -# CONNECTOR_CLICKUP_CLIENT_ID= -# CONNECTOR_CLICKUP_CLIENT_SECRET= -# CONNECTOR_VERCEL_CLIENT_ID= -# CONNECTOR_VERCEL_CLIENT_SECRET= -# CONNECTOR_VERCEL_INTEGRATION_SLUG= -# CONNECTOR_MONDAY_CLIENT_ID= -# CONNECTOR_MONDAY_CLIENT_SECRET= -# CONNECTOR_DATADOG_CLIENT_ID= -# CONNECTOR_DATADOG_CLIENT_SECRET= +# PROBOD_CONNECTOR_SLACK_CLIENT_ID= +# PROBOD_CONNECTOR_SLACK_CLIENT_SECRET= +# PROBOD_CONNECTOR_SLACK_SIGNING_SECRET= +# PROBOD_CONNECTOR_HUBSPOT_CLIENT_ID= +# PROBOD_CONNECTOR_HUBSPOT_CLIENT_SECRET= +# PROBOD_CONNECTOR_GITHUB_CLIENT_ID= +# PROBOD_CONNECTOR_GITHUB_CLIENT_SECRET= +# PROBOD_CONNECTOR_NOTION_CLIENT_ID= +# PROBOD_CONNECTOR_NOTION_CLIENT_SECRET= +# PROBOD_CONNECTOR_SENTRY_CLIENT_ID= +# PROBOD_CONNECTOR_SENTRY_CLIENT_SECRET= +# PROBOD_CONNECTOR_INTERCOM_CLIENT_ID= +# PROBOD_CONNECTOR_INTERCOM_CLIENT_SECRET= +# PROBOD_CONNECTOR_DOCUSIGN_CLIENT_ID= +# PROBOD_CONNECTOR_DOCUSIGN_CLIENT_SECRET= +# PROBOD_CONNECTOR_BREX_CLIENT_ID= +# PROBOD_CONNECTOR_BREX_CLIENT_SECRET= +# PROBOD_CONNECTOR_GITLAB_CLIENT_ID= +# PROBOD_CONNECTOR_GITLAB_CLIENT_SECRET= +# PROBOD_CONNECTOR_BITBUCKET_CLIENT_ID= +# PROBOD_CONNECTOR_BITBUCKET_CLIENT_SECRET= +# PROBOD_CONNECTOR_HEROKU_CLIENT_ID= +# PROBOD_CONNECTOR_HEROKU_CLIENT_SECRET= +# PROBOD_CONNECTOR_PAGERDUTY_CLIENT_ID= +# PROBOD_CONNECTOR_PAGERDUTY_CLIENT_SECRET= +# PROBOD_CONNECTOR_ASANA_CLIENT_ID= +# PROBOD_CONNECTOR_ASANA_CLIENT_SECRET= +# PROBOD_CONNECTOR_NETLIFY_CLIENT_ID= +# PROBOD_CONNECTOR_NETLIFY_CLIENT_SECRET= +# PROBOD_CONNECTOR_CLICKUP_CLIENT_ID= +# PROBOD_CONNECTOR_CLICKUP_CLIENT_SECRET= +# PROBOD_CONNECTOR_VERCEL_CLIENT_ID= +# PROBOD_CONNECTOR_VERCEL_CLIENT_SECRET= +# PROBOD_CONNECTOR_VERCEL_INTEGRATION_SLUG= +# PROBOD_CONNECTOR_MONDAY_CLIENT_ID= +# PROBOD_CONNECTOR_MONDAY_CLIENT_SECRET= +# PROBOD_CONNECTOR_DATADOG_CLIENT_ID= +# PROBOD_CONNECTOR_DATADOG_CLIENT_SECRET= # Zendesk OAuth requires a Zendesk-approved global OAuth client (Marketplace). -# CONNECTOR_ZENDESK_CLIENT_ID= -# CONNECTOR_ZENDESK_CLIENT_SECRET= +# PROBOD_CONNECTOR_ZENDESK_CLIENT_ID= +# PROBOD_CONNECTOR_ZENDESK_CLIENT_SECRET= # PostHog Cloud (US + EU) OAuth needs NO config: it uses the CIMD public-client # flow (no app registration, no client_secret), auto-enabled when this # deployment is publicly reachable at PROBOD_BASE_URL. Self-hosted PostHog uses @@ -140,9 +149,9 @@ # ── OAuth2 authorization server ─────────────────────────────────────── # Comma-separated HTTPS client metadata document URLs allowed for CIMD # OAuth clients (e.g. MCP connectors). Leave unset to disable CIMD. -# OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS=https://chatgpt.com/oauth/client.json,https://claude.ai/oauth/client.json +# PROBOD_OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS=https://chatgpt.com/oauth/client.json,https://claude.ai/oauth/client.json # ── Custom domains (Pebble ACME via compose) ────────────────────────── -# CUSTOM_DOMAINS_CNAME_TARGET=custom.getprobo.com -# ACME_DIRECTORY=https://localhost:14000/dir -# ACME_EMAIL=admin@probo.com +# PROBOD_CUSTOM_DOMAINS_CNAME_TARGET=custom.getprobo.com +# PROBOD_ACME_DIRECTORY=https://localhost:14000/dir +# PROBOD_ACME_EMAIL=admin@probo.com diff --git a/.github/workflows/make.yaml b/.github/workflows/make.yaml index 534ba6dd9..3966288c9 100644 --- a/.github/workflows/make.yaml +++ b/.github/workflows/make.yaml @@ -464,7 +464,7 @@ jobs: GOTESTSUM_FORMAT: "testname" GOTESTSUM_JUNITFILE: "junit-e2e.xml" run: | - ACME_ROOT_CA="$(cat compose/pebble/certs/rootCA.pem)" \ + PROBOD_ACME_ROOT_CA="$(cat compose/pebble/certs/rootCA.pem)" \ CGO_ENABLED=1 go tool gotestsum -- -race -cover -coverprofile=coverage.out -count=1 ./e2e/console/... - name: "Upload test results" uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7 diff --git a/GNUmakefile b/GNUmakefile index f3d8f9c7b..8120c8fe8 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -201,19 +201,18 @@ $(CFG_DEV_OAUTH2_KEY): cfg/dev.yaml: bin/probod-bootstrap $(CFG_DEV_OAUTH2_KEY) compose/pebble/certs/rootCA.pem $(wildcard $(DEV_ENV)) @$(MKDIR) $(@D) set -a; \ - PROBOD_ENCRYPTION_KEY="thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA="; \ - AUTH_COOKIE_SECRET="this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes"; \ - AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes"; \ - AUTH_COOKIE_SECURE=false; \ - OAUTH2_SERVER_SIGNING_KEY="$$($(CAT) $(CFG_DEV_OAUTH2_KEY))"; \ - API_CORS_ALLOWED_ORIGINS="http://localhost:8080,http://localhost:5173,http://localhost:5174"; \ - AWS_ACCESS_KEY_ID=probod; \ - AWS_SECRET_ACCESS_KEY=thisisnotasecret; \ - AWS_ENDPOINT=http://127.0.0.1:8333; \ - OPENAI_API_KEY=thisisnotasecret; \ - AGENT_THIRD_PARTY_VETTER_PROVIDER=openai; \ - ACME_DIRECTORY=https://localhost:14000/dir; \ - ACME_ROOT_CA="$$($(CAT) compose/pebble/certs/rootCA.pem)"; \ + PROBOD_ENCRYPTION_KEY="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; \ + PROBOD_AUTH_COOKIE_SECRET="this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes"; \ + PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes"; \ + PROBOD_AUTH_COOKIE_SECURE=false; \ + PROBOD_OAUTH2_SERVER_SIGNING_KEY="$$($(CAT) $(CFG_DEV_OAUTH2_KEY))"; \ + PROBOD_API_CORS_ALLOWED_ORIGINS="http://localhost:8080,http://localhost:5173,http://localhost:5174"; \ + PROBOD_AWS_ACCESS_KEY_ID=probod; \ + PROBOD_AWS_SECRET_ACCESS_KEY=thisisnotasecret; \ + PROBOD_AWS_ENDPOINT=http://127.0.0.1:8333; \ + PROBOD_OPENAI_API_KEY=thisisnotasecret; \ + PROBOD_AGENT_THIRD_PARTY_VETTER_PROVIDER=openai; \ + PROBOD_ACME_DIRECTORY=https://localhost:14000/dir; \ if [ -f $(DEV_ENV) ]; then . $(DEV_ENV); fi; \ set +a; \ ./bin/probod-bootstrap -output $@ diff --git a/cmd/probod-bootstrap/main.go b/cmd/probod-bootstrap/main.go index b5a60432a..2cd3853bb 100644 --- a/cmd/probod-bootstrap/main.go +++ b/cmd/probod-bootstrap/main.go @@ -38,7 +38,7 @@ func main() { return } - builder := bootstrap.NewBuilder(nil) + builder := bootstrap.NewBuilder(bootstrap.NewResolver(nil)) cfg, err := builder.Build() if err != nil { diff --git a/compose.prod.yaml b/compose.prod.yaml index a5339a981..18ea04328 100644 --- a/compose.prod.yaml +++ b/compose.prod.yaml @@ -4,42 +4,42 @@ services: environment: # Required secrets (use secure values in production) PROBOD_ENCRYPTION_KEY: ${PROBOD_ENCRYPTION_KEY} - AUTH_COOKIE_SECRET: ${AUTH_COOKIE_SECRET} - AUTH_PASSWORD_PEPPER: ${AUTH_PASSWORD_PEPPER} - TRUST_AUTH_TOKEN_SECRET: ${TRUST_AUTH_TOKEN_SECRET} + PROBOD_AUTH_COOKIE_SECRET: ${PROBOD_AUTH_COOKIE_SECRET} + PROBOD_AUTH_PASSWORD_PEPPER: ${PROBOD_AUTH_PASSWORD_PEPPER} + PROBOD_TRUST_AUTH_TOKEN_SECRET: ${PROBOD_TRUST_AUTH_TOKEN_SECRET} # Application settings PROBOD_BASE_URL: ${PROBOD_BASE_URL} - API_ADDR: ${API_ADDR} - API_CORS_ALLOWED_ORIGINS: ${API_CORS_ALLOWED_ORIGINS} + PROBOD_API_ADDR: ${PROBOD_API_ADDR} + PROBOD_API_CORS_ALLOWED_ORIGINS: ${PROBOD_API_CORS_ALLOWED_ORIGINS} # PostgreSQL database - PG_ADDR: "postgres:5432" - PG_USERNAME: "postgres" - PG_PASSWORD: "postgres" - PG_DATABASE: "probod" - PG_POOL_SIZE: "100" + PROBOD_PG_ADDR: "postgres:5432" + PROBOD_PG_USERNAME: "postgres" + PROBOD_PG_PASSWORD: "postgres" + PROBOD_PG_DATABASE: "probod" + PROBOD_PG_POOL_SIZE: "100" # S3-compatible storage (SeaweedFS) - AWS_REGION: "us-east-1" - AWS_BUCKET: "probod" - AWS_ACCESS_KEY_ID: "probod" - AWS_SECRET_ACCESS_KEY: "thisisnotasecret" - AWS_ENDPOINT: "http://seaweedfs:8333" - AWS_USE_PATH_STYLE: "true" + PROBOD_AWS_REGION: "us-east-1" + PROBOD_AWS_BUCKET: "probod" + PROBOD_AWS_ACCESS_KEY_ID: "probod" + PROBOD_AWS_SECRET_ACCESS_KEY: "thisisnotasecret" + PROBOD_AWS_ENDPOINT: "http://seaweedfs:8333" + PROBOD_AWS_USE_PATH_STYLE: "true" # Observability - Metrics & Tracing - METRICS_ADDR: "probo:8081" - TRACING_ADDR: "" + PROBOD_METRICS_ADDR: "probo:8081" + PROBOD_TRACING_ADDR: "" # Email notifications - SMTP_ADDR: "your.smtp.server:587" - SMTP_TLS_REQUIRED: "false" - MAILER_SENDER_NAME: "Probo" - MAILER_SENDER_EMAIL: "no-reply@notification.getprobo.com" + PROBOD_SMTP_ADDR: "your.smtp.server:587" + PROBOD_SMTP_TLS_REQUIRED: "false" + PROBOD_MAILER_SENDER_NAME: "Probo" + PROBOD_MAILER_SENDER_EMAIL: "no-reply@notification.getprobo.com" # Chrome for PDF generation - CHROME_DP_ADDR: "chrome:9222" + PROBOD_CHROME_DP_ADDR: "chrome:9222" ports: - "8080:8080" - "8081:8081" diff --git a/contrib/claude/config.md b/contrib/claude/config.md index 5ad466d8f..2e573f9c7 100644 --- a/contrib/claude/config.md +++ b/contrib/claude/config.md @@ -27,6 +27,7 @@ Go struct (pkg/probod/) │ ├─► bootstrap builder.go (env var → struct) │ │ + │ ├─► Resolver (aws:// secret-id refs + plaintext env literals) │ ├─► GNUmakefile dev-config (env vars → probod-bootstrap → cfg/dev.yaml) │ ├─► e2e/internal/testutil/ (env map → bootstrap.Build, tests) │ ├─► contrib/lima/provision.sh (env vars → probod-bootstrap) @@ -42,11 +43,11 @@ Go struct (pkg/probod/) ## Rules 1. **Never add a Go config field without updating every file in the checklist.** -2. **Env var naming** — follow the existing convention in `builder.go`: `SECTION_FIELD_NAME` (e.g. `AUTH_COOKIE_DOMAIN`, `CUSTOM_DOMAINS_RENEWAL_INTERVAL`). +2. **Env var naming** — `probod-bootstrap` reads every input from the process environment with a `PROBOD_` prefix (e.g. `PROBOD_AUTH_COOKIE_DOMAIN`, `PROBOD_CUSTOM_DOMAINS_RENEWAL_INTERVAL`). Use the full name in `builder.go`, Helm templates, and docs. 3. **Secrets** go through `secret.yaml` and are referenced via `secretKeyRef` in `deployment.yaml`. Non-secret values are set inline. 4. **`make dev-config`** writes `cfg/dev.yaml` via `probod-bootstrap` with safe, non-production defaults (plaintext passwords, `localhost`, `secure: false`). The generated file and the per-dev OAuth2 signing key (`cfg/.dev-oauth2-signing-key.pem`) are both gitignored. The recipe sources `.env` at the repo root if present so devs can override any env var without editing the `GNUmakefile`; keep `.env.example` in sync when you add or rename env vars. 5. **`e2e/internal/testutil/testutil.go`** builds the e2e config through `bootstrap.NewBuilder` with a test-only env-var map (different ports, `probod_test` DB, shorter intervals). Any new field whose test value differs from the bootstrap default must be added to that map. -6. **`provision.sh`** only sets env vars that differ from `builder.go` defaults (e.g. `PROBOD_BASE_URL`, `AUTH_COOKIE_DOMAIN`, `AUTH_COOKIE_SECURE`). If the new field's default is acceptable in the sandbox, no env var is needed. +6. **`provision.sh`** only sets env vars that differ from `builder.go` defaults (e.g. `PROBOD_BASE_URL`, `PROBOD_AUTH_COOKIE_DOMAIN`, `PROBOD_AUTH_COOKIE_SECURE`). If the new field's default is acceptable in the sandbox, no env var is needed. 7. **Helm `values.yaml`** exposes the field under the appropriate `probo.*` key with a sensible default. `values-production.yaml.example` includes it only when the production value differs or the user must set it. 8. **Optional features** (custom domains, SAML, connectors, tracing) are gated by `{{- if }}` blocks in the Helm templates; follow the same pattern for new optional fields. 9. **Bootstrap tests** (`pkg/bootstrap/builder_test.go`) must cover the new env var mapping. diff --git a/contrib/claude/e2e.md b/contrib/claude/e2e.md index 6d731a2ea..bef224ce6 100644 --- a/contrib/claude/e2e.md +++ b/contrib/claude/e2e.md @@ -2,10 +2,28 @@ E2E tests live in `e2e/console/` (package `console_test`) and run against a live `bin/probod` instance. The test infrastructure handles server lifecycle, authentication, and test data creation. +## Prerequisites + +E2e uses the local [Pebble](https://github.com/letsencrypt/pebble) ACME server over HTTPS. Pebble’s TLS certificate is minted with [mkcert](https://github.com/FiloSottile/mkcert); register mkcert’s root CA in your system trust store once per machine: + +```bash +mkcert -install +``` + +Without this step, probod cannot verify Pebble’s HTTPS endpoint when it registers an ACME account at startup. + +You also need the Docker stack running and `bin/probod` built. `make stack-up` generates Pebble TLS material under `compose/pebble/certs/` (via mkcert): + +```bash +make stack-up +make build +``` + +E2e config is built at test startup in `e2e/internal/testutil/testutil.go` (`generateConfig` → `probod-bootstrap`). It points ACME at Pebble but does not set `PROBOD_ACME_ROOT_CA`; local runs rely on the system trust store populated by `mkcert -install`. CI passes `PROBOD_ACME_ROOT_CA` in the workflow instead. + ## Running tests ```bash -make build # Build the binary (backend only) make test-e2e # Run all e2e tests ``` diff --git a/contrib/helm/README.md b/contrib/helm/README.md index eda37cd03..82493e506 100644 --- a/contrib/helm/README.md +++ b/contrib/helm/README.md @@ -322,6 +322,41 @@ spec: key: probo/db-password ``` +### Native AWS Secrets Manager (probod-bootstrap) + +Alternatively, `probod-bootstrap` can fetch secrets directly from AWS Secrets +Manager without External Secrets Operator. Point env vars at individual secrets +with the `aws://` prefix (e.g. +`PROBOD_ENCRYPTION_KEY=aws://probo/sandbox/probod/encryption_key`). The path +after `aws://` is the secret name or ARN; the plaintext `SecretString` is used +directly. Each env var can reference a different secret. Plain env values are +also supported for non-sensitive config. + +Grant the caller `secretsmanager:GetSecretValue` on each secret (EKS IRSA +example): + +```yaml +env: + - name: AWS_REGION + value: "us-east-1" + - name: PROBOD_BASE_URL + value: "https://app.example.com" + - name: PROBOD_ENCRYPTION_KEY + value: "aws://probo/sandbox/probod/encryption_key" + - name: PROBOD_AUTH_COOKIE_SECRET + value: "aws://probo/sandbox/probod/cookie_secret" + - name: PROBOD_AUTH_PASSWORD_PEPPER + value: "aws://probo/sandbox/probod/password_pepper" + - name: PROBOD_OAUTH2_SERVER_SIGNING_KEY + value: "aws://probo/sandbox/probod/oauth2_signing_key" +``` + +Each secret in AWS Secrets Manager stores a single plaintext value (for +example a base64 key, password, or PEM). + +When `PROBOD_ENCRYPTION_KEY` or another bootstrap env var is set (including +`aws://` references), the container entrypoint runs `probod-bootstrap`. + ## Full Values | Key | Type | Default | Description | diff --git a/contrib/helm/charts/probo/templates/deployment.yaml b/contrib/helm/charts/probo/templates/deployment.yaml index fb7941e6d..cfc310d8d 100644 --- a/contrib/helm/charts/probo/templates/deployment.yaml +++ b/contrib/helm/charts/probo/templates/deployment.yaml @@ -43,19 +43,19 @@ spec: - name: CONFIG_FILE value: "/data/probod/config.yml" # Observability - Metrics - - name: METRICS_ADDR + - name: PROBOD_METRICS_ADDR value: "0.0.0.0:{{ .Values.probo.metrics.port }}" {{- if .Values.probo.tracing.enabled }} # Observability - Tracing - - name: TRACING_ADDR + - name: PROBOD_TRACING_ADDR value: {{ .Values.probo.tracing.addr | quote }} - - name: TRACING_MAX_BATCH_SIZE + - name: PROBOD_TRACING_MAX_BATCH_SIZE value: {{ .Values.probo.tracing.maxBatchSize | quote }} - - name: TRACING_BATCH_TIMEOUT + - name: PROBOD_TRACING_BATCH_TIMEOUT value: {{ .Values.probo.tracing.batchTimeout | quote }} - - name: TRACING_EXPORT_TIMEOUT + - name: PROBOD_TRACING_EXPORT_TIMEOUT value: {{ .Values.probo.tracing.exportTimeout | quote }} - - name: TRACING_MAX_QUEUE_SIZE + - name: PROBOD_TRACING_MAX_QUEUE_SIZE value: {{ .Values.probo.tracing.maxQueueSize | quote }} {{- end }} # Application Configuration @@ -66,96 +66,96 @@ spec: secretKeyRef: name: {{ include "probo.fullname" . }} key: encryption-key - - name: CHROME_DP_ADDR + - name: PROBOD_CHROME_DP_ADDR value: {{ include "probo.chrome.addr" . | quote }} - - name: BRANDING + - name: PROBOD_BRANDING value: {{ .Values.probo.branding | quote }} # API Configuration - - name: API_ADDR + - name: PROBOD_API_ADDR value: ":{{ .Values.service.port }}" - - name: API_CORS_ALLOWED_ORIGINS + - name: PROBOD_API_CORS_ALLOWED_ORIGINS value: {{ join "," .Values.probo.cors.allowedOrigins | quote }} # PostgreSQL Database - - name: PG_ADDR + - name: PROBOD_PG_ADDR value: {{ printf "%s:%v" (include "probo.postgresql.host" .) (include "probo.postgresql.port" .) | quote }} - - name: PG_USERNAME + - name: PROBOD_PG_USERNAME value: {{ include "probo.postgresql.username" . | quote }} - - name: PG_PASSWORD + - name: PROBOD_PG_PASSWORD valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: db-password - - name: PG_DATABASE + - name: PROBOD_PG_DATABASE value: {{ include "probo.postgresql.database" . | quote }} - - name: PG_POOL_SIZE + - name: PROBOD_PG_POOL_SIZE value: {{ .Values.postgresql.poolSize | default "100" | quote }} - - name: PG_MIN_POOL_SIZE + - name: PROBOD_PG_MIN_POOL_SIZE value: {{ .Values.postgresql.minPoolSize | default "10" | quote }} - - name: PG_MAX_CONN_IDLE_TIME_SECONDS + - name: PROBOD_PG_MAX_CONN_IDLE_TIME_SECONDS value: {{ .Values.postgresql.maxConnIdleTimeSeconds | default "1800" | quote }} - - name: PG_MAX_CONN_LIFETIME_SECONDS + - name: PROBOD_PG_MAX_CONN_LIFETIME_SECONDS value: {{ .Values.postgresql.maxConnLifetimeSeconds | default "3600" | quote }} - - name: PG_MAX_CONN_LIFETIME_JITTER_SECONDS + - name: PROBOD_PG_MAX_CONN_LIFETIME_JITTER_SECONDS value: {{ .Values.postgresql.maxConnLifetimeJitterSeconds | default "300" | quote }} - - name: PG_HEALTH_CHECK_PERIOD_SECONDS + - name: PROBOD_PG_HEALTH_CHECK_PERIOD_SECONDS value: {{ .Values.postgresql.healthCheckPeriodSeconds | default "60" | quote }} {{- if .Values.postgresql.caBundle }} - - name: PG_CA_BUNDLE + - name: PROBOD_PG_CA_BUNDLE valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: pg-ca-bundle {{- else if .Values.postgresql.caBundlePath }} - - name: PG_CA_BUNDLE_PATH + - name: PROBOD_PG_CA_BUNDLE_PATH value: {{ .Values.postgresql.caBundlePath | quote }} {{- end }} # Authentication - - name: AUTH_DISABLE_SIGNUP + - name: PROBOD_AUTH_DISABLE_SIGNUP value: {{ .Values.probo.auth.disableSignup | quote }} - - name: AUTH_INVITATION_TOKEN_VALIDITY + - name: PROBOD_AUTH_INVITATION_TOKEN_VALIDITY value: {{ .Values.probo.auth.invitationTokenValidity | quote }} - - name: AUTH_COOKIE_NAME + - name: PROBOD_AUTH_COOKIE_NAME value: {{ .Values.probo.auth.cookieName | quote }} - - name: AUTH_COOKIE_DOMAIN + - name: PROBOD_AUTH_COOKIE_DOMAIN value: {{ .Values.probo.auth.cookieDomain | quote }} - - name: AUTH_COOKIE_SECRET + - name: PROBOD_AUTH_COOKIE_SECRET valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: cookie-secret - - name: AUTH_COOKIE_DURATION + - name: PROBOD_AUTH_COOKIE_DURATION value: {{ .Values.probo.auth.cookieDuration | quote }} - - name: AUTH_PASSWORD_PEPPER + - name: PROBOD_AUTH_PASSWORD_PEPPER valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: password-pepper - - name: OAUTH2_SERVER_SIGNING_KEY + - name: PROBOD_OAUTH2_SERVER_SIGNING_KEY valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: oauth2-signing-key {{- if .Values.probo.oauth2.cimdAllowedClientIds }} - - name: OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS + - name: PROBOD_OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS value: {{ join "," .Values.probo.oauth2.cimdAllowedClientIds | quote }} {{- end }} - - name: AUTH_PASSWORD_ITERATIONS + - name: PROBOD_AUTH_PASSWORD_ITERATIONS value: {{ .Values.probo.auth.passwordIterations | quote }} {{- if .Values.probo.saml.enabled }} # SAML Authentication - - name: SAML_SESSION_DURATION + - name: PROBOD_SAML_SESSION_DURATION value: {{ .Values.probo.saml.sessionDuration | quote }} - - name: SAML_CLEANUP_INTERVAL_SECONDS + - name: PROBOD_SAML_CLEANUP_INTERVAL_SECONDS value: {{ .Values.probo.saml.cleanupIntervalSeconds | quote }} {{- if .Values.probo.saml.certificate }} - - name: SAML_CERTIFICATE + - name: PROBOD_SAML_CERTIFICATE valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: saml-certificate {{- end }} {{- if .Values.probo.saml.privateKey }} - - name: SAML_PRIVATE_KEY + - name: PROBOD_SAML_PRIVATE_KEY valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} @@ -163,93 +163,93 @@ spec: {{- end }} {{- end }} # Trust Center Authentication - - name: TRUST_AUTH_COOKIE_NAME + - name: PROBOD_TRUST_AUTH_COOKIE_NAME value: {{ .Values.probo.trustAuth.cookieName | quote }} - - name: TRUST_AUTH_COOKIE_DOMAIN + - name: PROBOD_TRUST_AUTH_COOKIE_DOMAIN value: {{ .Values.probo.trustAuth.cookieDomain | quote }} - - name: TRUST_AUTH_COOKIE_DURATION + - name: PROBOD_TRUST_AUTH_COOKIE_DURATION value: {{ .Values.probo.trustAuth.cookieDuration | quote }} - - name: TRUST_AUTH_TOKEN_DURATION + - name: PROBOD_TRUST_AUTH_TOKEN_DURATION value: {{ .Values.probo.trustAuth.tokenDuration | quote }} - - name: TRUST_AUTH_REPORT_URL_DURATION + - name: PROBOD_TRUST_AUTH_REPORT_URL_DURATION value: {{ .Values.probo.trustAuth.reportUrlDuration | quote }} - - name: TRUST_AUTH_TOKEN_SECRET + - name: PROBOD_TRUST_AUTH_TOKEN_SECRET valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: trust-token-secret - - name: TRUST_AUTH_SCOPE + - name: PROBOD_TRUST_AUTH_SCOPE value: {{ .Values.probo.trustAuth.scope | quote }} - - name: TRUST_AUTH_TOKEN_TYPE + - name: PROBOD_TRUST_AUTH_TOKEN_TYPE value: {{ .Values.probo.trustAuth.tokenType | quote }} # AWS / S3 Storage - - name: AWS_REGION + - name: PROBOD_AWS_REGION value: {{ .Values.s3.region | quote }} - - name: AWS_BUCKET + - name: PROBOD_AWS_BUCKET value: {{ .Values.s3.bucket | quote }} - - name: AWS_ACCESS_KEY_ID + - name: PROBOD_AWS_ACCESS_KEY_ID valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: s3-access-key - - name: AWS_SECRET_ACCESS_KEY + - name: PROBOD_AWS_SECRET_ACCESS_KEY valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: s3-secret-key {{- $s3Endpoint := include "probo.s3.endpoint" . }} {{- if $s3Endpoint }} - - name: AWS_ENDPOINT + - name: PROBOD_AWS_ENDPOINT value: {{ $s3Endpoint | quote }} {{- end }} {{- if .Values.s3.usePathStyle }} - - name: AWS_USE_PATH_STYLE + - name: PROBOD_AWS_USE_PATH_STYLE value: "true" {{- end }} # Email (SMTP) - - name: MAILER_SENDER_NAME + - name: PROBOD_MAILER_SENDER_NAME value: {{ .Values.probo.mailer.senderName | quote }} - - name: MAILER_SENDER_EMAIL + - name: PROBOD_MAILER_SENDER_EMAIL value: {{ .Values.probo.mailer.senderEmail | quote }} - - name: SMTP_ADDR + - name: PROBOD_SMTP_ADDR value: {{ .Values.probo.mailer.smtp.addr | quote }} {{- if .Values.probo.mailer.smtp.user }} - - name: SMTP_USER + - name: PROBOD_SMTP_USER valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: smtp-user {{- end }} {{- if .Values.probo.mailer.smtp.password }} - - name: SMTP_PASSWORD + - name: PROBOD_SMTP_PASSWORD valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: smtp-password {{- end }} - - name: SMTP_TLS_REQUIRED + - name: PROBOD_SMTP_TLS_REQUIRED value: {{ .Values.probo.mailer.smtp.tlsRequired | quote }} {{- if .Values.probo.mailer.smtp.helloName }} - - name: SMTP_HELLO_NAME + - name: PROBOD_SMTP_HELLO_NAME value: {{ .Values.probo.mailer.smtp.helloName | quote }} {{- end }} # OpenAI Integration {{- if .Values.probo.openai.apiKey }} - - name: OPENAI_API_KEY + - name: PROBOD_OPENAI_API_KEY valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} key: openai-api-key - - name: OPENAI_TEMPERATURE + - name: PROBOD_OPENAI_TEMPERATURE value: {{ .Values.probo.openai.temperature | quote }} - - name: OPENAI_MODEL_NAME + - name: PROBOD_OPENAI_MODEL_NAME value: {{ .Values.probo.openai.modelName | quote }} - - name: OPENAI_MAX_TOKENS + - name: PROBOD_OPENAI_MAX_TOKENS value: {{ .Values.probo.openai.maxTokens | quote }} {{- end }} # Agent Tools {{- if .Values.probo.agentTools.firecrawlApiKey }} - - name: FIRECRAWL_API_KEY + - name: PROBOD_FIRECRAWL_API_KEY valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} @@ -257,193 +257,193 @@ spec: {{- end }} # Third-Party Vetter Agent {{- if .Values.probo.thirdPartyVetter.provider }} - - name: AGENT_THIRD_PARTY_VETTER_PROVIDER + - name: PROBOD_AGENT_THIRD_PARTY_VETTER_PROVIDER value: {{ .Values.probo.thirdPartyVetter.provider | quote }} {{- end }} {{- if .Values.probo.thirdPartyVetter.modelName }} - - name: AGENT_THIRD_PARTY_VETTER_MODEL_NAME + - name: PROBOD_AGENT_THIRD_PARTY_VETTER_MODEL_NAME value: {{ .Values.probo.thirdPartyVetter.modelName | quote }} {{- end }} {{- if .Values.probo.thirdPartyVetter.temperature }} - - name: AGENT_THIRD_PARTY_VETTER_TEMPERATURE + - name: PROBOD_AGENT_THIRD_PARTY_VETTER_TEMPERATURE value: {{ .Values.probo.thirdPartyVetter.temperature | quote }} {{- end }} {{- if .Values.probo.thirdPartyVetter.maxTokens }} - - name: AGENT_THIRD_PARTY_VETTER_MAX_TOKENS + - name: PROBOD_AGENT_THIRD_PARTY_VETTER_MAX_TOKENS value: {{ .Values.probo.thirdPartyVetter.maxTokens | quote }} {{- end }} # Third-Party Vetting Worker {{- if .Values.probo.thirdPartyVettingWorker.interval }} - - name: THIRD_PARTY_VETTING_INTERVAL + - name: PROBOD_THIRD_PARTY_VETTING_INTERVAL value: {{ .Values.probo.thirdPartyVettingWorker.interval | quote }} {{- end }} {{- if .Values.probo.thirdPartyVettingWorker.maxConcurrency }} - - name: THIRD_PARTY_VETTING_MAX_CONCURRENCY + - name: PROBOD_THIRD_PARTY_VETTING_MAX_CONCURRENCY value: {{ .Values.probo.thirdPartyVettingWorker.maxConcurrency | quote }} {{- end }} {{- if .Values.probo.thirdPartyVettingWorker.staleAfter }} - - name: THIRD_PARTY_VETTING_STALE_AFTER + - name: PROBOD_THIRD_PARTY_VETTING_STALE_AFTER value: {{ .Values.probo.thirdPartyVettingWorker.staleAfter | quote }} {{- end }} # Third-party Disambiguation Agent {{- if .Values.probo.thirdPartyDisambiguation.provider }} - - name: AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER + - name: PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER value: {{ .Values.probo.thirdPartyDisambiguation.provider | quote }} {{- end }} {{- if .Values.probo.thirdPartyDisambiguation.modelName }} - - name: AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME + - name: PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME value: {{ .Values.probo.thirdPartyDisambiguation.modelName | quote }} {{- end }} {{- if .Values.probo.thirdPartyDisambiguation.temperature }} - - name: AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE + - name: PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE value: {{ .Values.probo.thirdPartyDisambiguation.temperature | quote }} {{- end }} {{- if .Values.probo.thirdPartyDisambiguation.maxTokens }} - - name: AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS + - name: PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS value: {{ .Values.probo.thirdPartyDisambiguation.maxTokens | quote }} {{- end }} # Tracker Mapping Agent {{- if .Values.probo.trackerMapping.provider }} - - name: AGENT_TRACKER_MAPPING_PROVIDER + - name: PROBOD_AGENT_TRACKER_MAPPING_PROVIDER value: {{ .Values.probo.trackerMapping.provider | quote }} {{- end }} {{- if .Values.probo.trackerMapping.modelName }} - - name: AGENT_TRACKER_MAPPING_MODEL_NAME + - name: PROBOD_AGENT_TRACKER_MAPPING_MODEL_NAME value: {{ .Values.probo.trackerMapping.modelName | quote }} {{- end }} {{- if .Values.probo.trackerMapping.temperature }} - - name: AGENT_TRACKER_MAPPING_TEMPERATURE + - name: PROBOD_AGENT_TRACKER_MAPPING_TEMPERATURE value: {{ .Values.probo.trackerMapping.temperature | quote }} {{- end }} {{- if .Values.probo.trackerMapping.maxTokens }} - - name: AGENT_TRACKER_MAPPING_MAX_TOKENS + - name: PROBOD_AGENT_TRACKER_MAPPING_MAX_TOKENS value: {{ .Values.probo.trackerMapping.maxTokens | quote }} {{- end }} # Tracker Enrichment Agent {{- if .Values.probo.trackerEnrichment.provider }} - - name: AGENT_TRACKER_ENRICHMENT_PROVIDER + - name: PROBOD_AGENT_TRACKER_ENRICHMENT_PROVIDER value: {{ .Values.probo.trackerEnrichment.provider | quote }} {{- end }} {{- if .Values.probo.trackerEnrichment.modelName }} - - name: AGENT_TRACKER_ENRICHMENT_MODEL_NAME + - name: PROBOD_AGENT_TRACKER_ENRICHMENT_MODEL_NAME value: {{ .Values.probo.trackerEnrichment.modelName | quote }} {{- end }} {{- if .Values.probo.trackerEnrichment.temperature }} - - name: AGENT_TRACKER_ENRICHMENT_TEMPERATURE + - name: PROBOD_AGENT_TRACKER_ENRICHMENT_TEMPERATURE value: {{ .Values.probo.trackerEnrichment.temperature | quote }} {{- end }} {{- if .Values.probo.trackerEnrichment.maxTokens }} - - name: AGENT_TRACKER_ENRICHMENT_MAX_TOKENS + - name: PROBOD_AGENT_TRACKER_ENRICHMENT_MAX_TOKENS value: {{ .Values.probo.trackerEnrichment.maxTokens | quote }} {{- end }} # Tracker Mapping Worker {{- if .Values.probo.trackerMappingWorker.interval }} - - name: TRACKER_MAPPING_INTERVAL + - name: PROBOD_TRACKER_MAPPING_INTERVAL value: {{ .Values.probo.trackerMappingWorker.interval | quote }} {{- end }} {{- if .Values.probo.trackerMappingWorker.maxConcurrency }} - - name: TRACKER_MAPPING_MAX_CONCURRENCY + - name: PROBOD_TRACKER_MAPPING_MAX_CONCURRENCY value: {{ .Values.probo.trackerMappingWorker.maxConcurrency | quote }} {{- end }} {{- if .Values.probo.trackerMappingWorker.staleAfter }} - - name: TRACKER_MAPPING_STALE_AFTER + - name: PROBOD_TRACKER_MAPPING_STALE_AFTER value: {{ .Values.probo.trackerMappingWorker.staleAfter | quote }} {{- end }} {{- if .Values.probo.trackerMappingWorker.agentTimeout }} - - name: TRACKER_MAPPING_AGENT_TIMEOUT + - name: PROBOD_TRACKER_MAPPING_AGENT_TIMEOUT value: {{ .Values.probo.trackerMappingWorker.agentTimeout | quote }} {{- end }} {{- if .Values.probo.trackerMappingWorker.agentMaxTurns }} - - name: TRACKER_MAPPING_AGENT_MAX_TURNS + - name: PROBOD_TRACKER_MAPPING_AGENT_MAX_TURNS value: {{ .Values.probo.trackerMappingWorker.agentMaxTurns | quote }} {{- end }} {{- if .Values.probo.trackerMappingWorker.disambiguationAgentTimeout }} - - name: TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT + - name: PROBOD_TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT value: {{ .Values.probo.trackerMappingWorker.disambiguationAgentTimeout | quote }} {{- end }} # Common Pattern Enrichment Worker {{- if .Values.probo.commonPatternEnrichmentWorker.interval }} - - name: COMMON_PATTERN_ENRICHMENT_INTERVAL + - name: PROBOD_COMMON_PATTERN_ENRICHMENT_INTERVAL value: {{ .Values.probo.commonPatternEnrichmentWorker.interval | quote }} {{- end }} {{- if .Values.probo.commonPatternEnrichmentWorker.maxConcurrency }} - - name: COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY + - name: PROBOD_COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY value: {{ .Values.probo.commonPatternEnrichmentWorker.maxConcurrency | quote }} {{- end }} {{- if .Values.probo.commonPatternEnrichmentWorker.staleAfter }} - - name: COMMON_PATTERN_ENRICHMENT_STALE_AFTER + - name: PROBOD_COMMON_PATTERN_ENRICHMENT_STALE_AFTER value: {{ .Values.probo.commonPatternEnrichmentWorker.staleAfter | quote }} {{- end }} {{- if .Values.probo.commonPatternEnrichmentWorker.agentTimeout }} - - name: COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT + - name: PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT value: {{ .Values.probo.commonPatternEnrichmentWorker.agentTimeout | quote }} {{- end }} {{- if .Values.probo.commonPatternEnrichmentWorker.agentMaxTurns }} - - name: COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS + - name: PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS value: {{ .Values.probo.commonPatternEnrichmentWorker.agentMaxTurns | quote }} {{- end }} # Common Third Party Enrichment Agent {{- if .Values.probo.commonThirdPartyEnrichment.provider }} - - name: AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER + - name: PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER value: {{ .Values.probo.commonThirdPartyEnrichment.provider | quote }} {{- end }} {{- if .Values.probo.commonThirdPartyEnrichment.modelName }} - - name: AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME + - name: PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME value: {{ .Values.probo.commonThirdPartyEnrichment.modelName | quote }} {{- end }} {{- if .Values.probo.commonThirdPartyEnrichment.temperature }} - - name: AGENT_COMMON_THIRD_PARTY_ENRICHMENT_TEMPERATURE + - name: PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_TEMPERATURE value: {{ .Values.probo.commonThirdPartyEnrichment.temperature | quote }} {{- end }} {{- if .Values.probo.commonThirdPartyEnrichment.maxTokens }} - - name: AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS + - name: PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS value: {{ .Values.probo.commonThirdPartyEnrichment.maxTokens | quote }} {{- end }} # Common Third Party Enrichment Worker {{- if .Values.probo.commonThirdPartyEnrichmentWorker.interval }} - - name: COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL + - name: PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL value: {{ .Values.probo.commonThirdPartyEnrichmentWorker.interval | quote }} {{- end }} {{- if .Values.probo.commonThirdPartyEnrichmentWorker.maxConcurrency }} - - name: COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY + - name: PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY value: {{ .Values.probo.commonThirdPartyEnrichmentWorker.maxConcurrency | quote }} {{- end }} {{- if .Values.probo.commonThirdPartyEnrichmentWorker.staleAfter }} - - name: COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER + - name: PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER value: {{ .Values.probo.commonThirdPartyEnrichmentWorker.staleAfter | quote }} {{- end }} {{- if .Values.probo.commonThirdPartyEnrichmentWorker.agentTimeout }} - - name: COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT + - name: PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT value: {{ .Values.probo.commonThirdPartyEnrichmentWorker.agentTimeout | quote }} {{- end }} {{- if .Values.probo.commonThirdPartyEnrichmentWorker.agentMaxTurns }} - - name: COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS + - name: PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS value: {{ .Values.probo.commonThirdPartyEnrichmentWorker.agentMaxTurns | quote }} {{- end }} {{- if ne .Values.probo.commonThirdPartyEnrichmentWorker.confidenceThreshold nil }} - - name: COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD + - name: PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD value: {{ .Values.probo.commonThirdPartyEnrichmentWorker.confidenceThreshold | quote }} {{- end }} {{- if .Values.probo.commonThirdPartyEnrichmentWorker.maxAttempts }} - - name: COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS + - name: PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS value: {{ .Values.probo.commonThirdPartyEnrichmentWorker.maxAttempts | quote }} {{- end }} # Custom Domains {{- if .Values.probo.customDomains.enabled }} - - name: CUSTOM_DOMAINS_RENEWAL_INTERVAL + - name: PROBOD_CUSTOM_DOMAINS_RENEWAL_INTERVAL value: {{ .Values.probo.customDomains.renewalInterval | quote }} - - name: CUSTOM_DOMAINS_PROVISION_INTERVAL + - name: PROBOD_CUSTOM_DOMAINS_PROVISION_INTERVAL value: {{ .Values.probo.customDomains.provisionInterval | quote }} - - name: CUSTOM_DOMAINS_CNAME_TARGET + - name: PROBOD_CUSTOM_DOMAINS_CNAME_TARGET value: {{ .Values.probo.customDomains.cnameTarget | quote }} - - name: ACME_DIRECTORY + - name: PROBOD_ACME_DIRECTORY value: {{ .Values.probo.customDomains.acme.directory | quote }} - - name: ACME_EMAIL + - name: PROBOD_ACME_EMAIL value: {{ .Values.probo.customDomains.acme.email | quote }} - - name: ACME_KEY_TYPE + - name: PROBOD_ACME_KEY_TYPE value: {{ .Values.probo.customDomains.acme.keyType | quote }} {{- if .Values.probo.customDomains.acme.accountKey }} - - name: ACME_ACCOUNT_KEY + - name: PROBOD_ACME_ACCOUNT_KEY valueFrom: secretKeyRef: name: {{ include "probo.fullname" . }} @@ -453,30 +453,30 @@ spec: # Connectors (Slack OAuth2) {{- range .Values.probo.connectors }} {{- if eq .name "slack" }} - - name: CONNECTOR_SLACK_CLIENT_ID + - name: PROBOD_CONNECTOR_SLACK_CLIENT_ID valueFrom: secretKeyRef: name: {{ include "probo.fullname" $ }} key: connector-slack-client-id - - name: CONNECTOR_SLACK_CLIENT_SECRET + - name: PROBOD_CONNECTOR_SLACK_CLIENT_SECRET valueFrom: secretKeyRef: name: {{ include "probo.fullname" $ }} key: connector-slack-client-secret {{- if .config.redirectUri }} - - name: CONNECTOR_SLACK_REDIRECT_URI + - name: PROBOD_CONNECTOR_SLACK_REDIRECT_URI value: {{ .config.redirectUri | quote }} {{- end }} {{- if .config.authUrl }} - - name: CONNECTOR_SLACK_AUTH_URL + - name: PROBOD_CONNECTOR_SLACK_AUTH_URL value: {{ .config.authUrl | quote }} {{- end }} {{- if .config.tokenUrl }} - - name: CONNECTOR_SLACK_TOKEN_URL + - name: PROBOD_CONNECTOR_SLACK_TOKEN_URL value: {{ .config.tokenUrl | quote }} {{- end }} {{- if .config.signingSecret }} - - name: CONNECTOR_SLACK_SIGNING_SECRET + - name: PROBOD_CONNECTOR_SLACK_SIGNING_SECRET valueFrom: secretKeyRef: name: {{ include "probo.fullname" $ }} diff --git a/contrib/lima/provision.sh b/contrib/lima/provision.sh index 0bc9d9ae7..132b3b882 100755 --- a/contrib/lima/provision.sh +++ b/contrib/lima/provision.sh @@ -114,21 +114,21 @@ if [ -f /workspace/.sandbox.env ]; then fi PROBOD_BASE_URL="http://${VM_IP}:8080" \ -AUTH_COOKIE_DOMAIN="${VM_IP}" \ -AUTH_COOKIE_SECURE=false \ -AUTH_COOKIE_SECRET="this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes" \ -AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes" \ +PROBOD_AUTH_COOKIE_DOMAIN="${VM_IP}" \ +PROBOD_AUTH_COOKIE_SECURE=false \ +PROBOD_AUTH_COOKIE_SECRET="this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes" \ +PROBOD_AUTH_PASSWORD_PEPPER="this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes" \ PROBOD_ENCRYPTION_KEY="thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=" \ -OAUTH2_SERVER_SIGNING_KEY="$(cat "${OAUTH2_SIGNING_KEY_PATH}")" \ -API_CORS_ALLOWED_ORIGINS="http://${VM_IP}:8080,http://${VM_IP}:5173,http://${VM_IP}:5174" \ -AWS_ENDPOINT="http://127.0.0.1:8333" \ -AWS_ACCESS_KEY_ID="probod" \ -AWS_SECRET_ACCESS_KEY="thisisnotasecret" \ -AWS_USE_PATH_STYLE=true \ -ACME_DIRECTORY="https://127.0.0.1:14000/dir" \ -ACME_EMAIL="admin@probo.com" \ -ACME_KEY_TYPE="EC256" \ -ACME_ROOT_CA="$(cat /workspace/compose/pebble/certs/rootCA.pem)" \ +PROBOD_OAUTH2_SERVER_SIGNING_KEY="$(cat "${OAUTH2_SIGNING_KEY_PATH}")" \ +PROBOD_API_CORS_ALLOWED_ORIGINS="http://${VM_IP}:8080,http://${VM_IP}:5173,http://${VM_IP}:5174" \ +PROBOD_AWS_ENDPOINT="http://127.0.0.1:8333" \ +PROBOD_AWS_ACCESS_KEY_ID="probod" \ +PROBOD_AWS_SECRET_ACCESS_KEY="thisisnotasecret" \ +PROBOD_AWS_USE_PATH_STYLE=true \ +PROBOD_ACME_DIRECTORY="https://127.0.0.1:14000/dir" \ +PROBOD_ACME_EMAIL="admin@probo.com" \ +PROBOD_ACME_KEY_TYPE="EC256" \ +PROBOD_ACME_ROOT_CA="$(cat /workspace/compose/pebble/certs/rootCA.pem)" \ /workspace/bin/probod-bootstrap -output /etc/probod/config.yml # probod runs as ${LIMA_USER} but bootstrap writes config.yml as root with 0600 diff --git a/e2e/internal/testutil/testutil.go b/e2e/internal/testutil/testutil.go index a2ae2ed3e..0265b3c52 100644 --- a/e2e/internal/testutil/testutil.go +++ b/e2e/internal/testutil/testutil.go @@ -256,68 +256,68 @@ func generateConfig() (string, error) { env := map[string]string{ // Required. - "PROBOD_ENCRYPTION_KEY": "thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "AUTH_COOKIE_SECRET": "this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes", - "AUTH_PASSWORD_PEPPER": "this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes", - "OAUTH2_SERVER_SIGNING_KEY": oauth2SigningKey, + "PROBOD_ENCRYPTION_KEY": "thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "PROBOD_AUTH_COOKIE_SECRET": "this-is-a-secure-secret-for-cookie-signing-at-least-32-bytes", + "PROBOD_AUTH_PASSWORD_PEPPER": "this-is-a-secure-pepper-for-password-hashing-at-least-32-bytes", + "PROBOD_OAUTH2_SERVER_SIGNING_KEY": oauth2SigningKey, // Unit. - "METRICS_ADDR": "localhost:19081", - "TRACING_ADDR": "localhost:14317", + "PROBOD_METRICS_ADDR": "localhost:19081", + "PROBOD_TRACING_ADDR": "localhost:14317", // Probod base. "PROBOD_BASE_URL": "http://localhost:18080", // API. - "API_ADDR": "localhost:18080", - "API_CORS_ALLOWED_ORIGINS": "http://localhost:18080", + "PROBOD_API_ADDR": "localhost:18080", + "PROBOD_API_CORS_ALLOWED_ORIGINS": "http://localhost:18080", // PG. - "PG_DATABASE": "probod_test", - "PG_POOL_SIZE": "10", - "PG_MIN_POOL_SIZE": "1", + "PROBOD_PG_DATABASE": "probod_test", + "PROBOD_PG_POOL_SIZE": "10", + "PROBOD_PG_MIN_POOL_SIZE": "1", // Auth. - "AUTH_COOKIE_SECURE": "false", - "AUTH_PASSWORD_ITERATIONS": "600000", + "PROBOD_AUTH_COOKIE_SECURE": "false", + "PROBOD_AUTH_PASSWORD_ITERATIONS": "600000", // OAuth2 server durations kept small for faster e2e flows. - "OAUTH2_SERVER_ACCESS_TOKEN_DURATION": "10", - "OAUTH2_SERVER_REFRESH_TOKEN_DURATION": "10", - "OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION": "5", - "OAUTH2_SERVER_DEVICE_CODE_DURATION": "15", + "PROBOD_OAUTH2_SERVER_ACCESS_TOKEN_DURATION": "10", + "PROBOD_OAUTH2_SERVER_REFRESH_TOKEN_DURATION": "10", + "PROBOD_OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION": "5", + "PROBOD_OAUTH2_SERVER_DEVICE_CODE_DURATION": "15", // Trust center. - "TRUST_CENTER_HTTP_ADDR": ":10080", - "TRUST_CENTER_HTTPS_ADDR": ":10443", + "PROBOD_TRUST_CENTER_HTTP_ADDR": ":10080", + "PROBOD_TRUST_CENTER_HTTPS_ADDR": ":10443", // AWS / S3 (SeaweedFS). - "AWS_BUCKET": "probod-test", - "AWS_ACCESS_KEY_ID": "probod", - "AWS_SECRET_ACCESS_KEY": "thisisnotasecret", - "AWS_ENDPOINT": "http://127.0.0.1:8333", + "PROBOD_AWS_BUCKET": "probod-test", + "PROBOD_AWS_ACCESS_KEY_ID": "probod", + "PROBOD_AWS_SECRET_ACCESS_KEY": "thisisnotasecret", + "PROBOD_AWS_ENDPOINT": "http://127.0.0.1:8333", // Mailer. - "MAILER_SENDER_NAME": "Probo Test", - "MAILER_SENDER_EMAIL": "no-reply@test.getprobo.com", - "MAILER_INTERVAL": "1", + "PROBOD_MAILER_SENDER_NAME": "Probo Test", + "PROBOD_MAILER_SENDER_EMAIL": "no-reply@test.getprobo.com", + "PROBOD_MAILER_INTERVAL": "1", // LLM. - "OPENAI_API_KEY": "thisisnotasecret", + "PROBOD_OPENAI_API_KEY": "thisisnotasecret", // Custom domains. - "CUSTOM_DOMAINS_CNAME_TARGET": "custom.test.getprobo.com", - "ACME_DIRECTORY": "https://localhost:14000/dir", - "ACME_EMAIL": "admin@test.getprobo.com", + "PROBOD_CUSTOM_DOMAINS_CNAME_TARGET": "custom.test.getprobo.com", + "PROBOD_ACME_DIRECTORY": "https://localhost:14000/dir", + "PROBOD_ACME_EMAIL": "admin@test.getprobo.com", } - builder := bootstrap.NewBuilder(func(key string) string { + builder := bootstrap.NewBuilder(bootstrap.NewResolver(func(key string) string { if v, ok := env[key]; ok { return v } return os.Getenv(key) - }) + })) cfg, err := builder.Build() if err != nil { diff --git a/entrypoint.sh b/entrypoint.sh index 6dd836cfc..da160f144 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,6 +5,7 @@ set -e CONFIG_FILE="${CONFIG_FILE:-/etc/probod/config.yml}" # If bootstrap env vars are set, always (re)generate the config from them. +# This includes literal values and aws:// Secrets Manager references. # This ensures that updated env vars take effect even when a stale config # file exists on a persistent volume. When no env vars are present, fall # back to an existing config file (e.g., mounted from a ConfigMap). diff --git a/go.mod b/go.mod index 0ec09d225..126cd4266 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,12 @@ require ( codeberg.org/miekg/dns v0.6.81 github.com/99designs/gqlgen v0.17.90 github.com/anthropics/anthropic-sdk-go v1.48.0 - github.com/aws/aws-sdk-go-v2 v1.41.12 + github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2/config v1.32.23 github.com/aws/aws-sdk-go-v2/credentials v1.19.22 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.53.4 github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2 + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.3 github.com/brianvoe/gofakeit/v7 v7.15.0 github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 @@ -159,8 +160,8 @@ require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.21 // indirect diff --git a/go.sum b/go.sum index 5e57579d9..3ceb603d3 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.41.12 h1:DIKX2c31ekm9RA2D9FBj1EWXx++9AdAqRw+e78Tq2Ck= -github.com/aws/aws-sdk-go-v2 v1.41.12/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= +github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA= +github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= github.com/aws/aws-sdk-go-v2/config v1.32.23 h1:PYDobtcsJXK6bQe9I8RQk6s19Bz3xa3xRU08Hy1Em3Y= @@ -71,10 +71,10 @@ github.com/aws/aws-sdk-go-v2/credentials v1.19.22 h1:SHfH6wyPsEgG7fVsi5rQxWEt7tu github.com/aws/aws-sdk-go-v2/credentials v1.19.22/go.mod h1:54nO8lKD4aQPOntM/VTWjnR+DYzTwx0YkSMZMhAgewQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28 h1:b+kcDejJrXc30zU/w8Tc9klISwaO5wh+6T0sMBdDoHM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.28/go.mod h1:LnI62O9GnSv6GcuLXxOYqlq0C8EmxMcgnF6m7LdYuOY= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28 h1:Xf2j7NdVcUKomlZ4iihOP4AZ3Fzlr8h4yKpXeP+OFPg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.28/go.mod h1:O8cDo1dW63jU7ki//kRe1z+tLGcpnD1jrouitsQddDw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28 h1:KqIfN9kpkKkcBqBbNpNGTIrXO6ExTUvFKvXkC+YAzVo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.28/go.mod h1:uxtQiKvLtNS4iXVsH2McVD/ls8FKN/uUhe1hGxPjrw0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29 h1:VkE9FuzTQVjBBrnj4+oCdxCLFIz7aqLYKUCjtvxVcOs= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.29/go.mod h1:H32Z2Qth9b+9LqjyBsCnozMQ8H2N7YBUDVXwbs0iggg= github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.53.4 h1:gNpZEJpHw/ANDvgAk2Z9yaiLmuFWfDnjZmzXZBMfhaw= @@ -91,6 +91,8 @@ github.com/aws/aws-sdk-go-v2/service/kms v1.52.0 h1:QNtg+Mtj1zmepk568+UKBD5DFfqh github.com/aws/aws-sdk-go-v2/service/kms v1.52.0/go.mod h1:Y0+uxvxz6ib4KktRdK0V4X45Vcs/JyYoz8H71pO8xeI= github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2 h1:b4ikkRk22T4xYkEgaWc3Voe+3xbt5YbbFhNehOWyUiY= github.com/aws/aws-sdk-go-v2/service/s3 v1.103.2/go.mod h1:Gp7eHZ0NZ8ZK5RXpoIUp/C8OeAmJqpCgdwEK1D/QOek= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.3 h1:L9gPLf3sFH1/ao3oB2QZcaX1xGYi8hj+WJlsf3/dN+M= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.42.3/go.mod h1:9DKRlwDCw2OUDlyCIFcQCroL5M0mQTUU9qW8JEDcXmI= github.com/aws/aws-sdk-go-v2/service/signin v1.1.4 h1:YcpVyIPLCbiypN6KSphijN5fC7DDjX114SqA7prnnxg= github.com/aws/aws-sdk-go-v2/service/signin v1.1.4/go.mod h1:5ZICS++oFTRPfa1GsBqFDWX/8WamZ/QQOcCzIuU/zLw= github.com/aws/aws-sdk-go-v2/service/sso v1.31.2 h1:ySNWu7TPmj5fKFIa1GYvX+Ddxd5ccruqC20aMNuyWDM= diff --git a/pkg/bootstrap/builder.go b/pkg/bootstrap/builder.go index 878c768ce..36846c056 100644 --- a/pkg/bootstrap/builder.go +++ b/pkg/bootstrap/builder.go @@ -17,7 +17,6 @@ package bootstrap import ( "fmt" "os" - "strconv" "strings" "go.probo.inc/probo/pkg/probodconfig" @@ -26,18 +25,18 @@ import ( type EnvGetter func(key string) string type Builder struct { - getEnv EnvGetter + resolver *Resolver samlCertificate string samlPrivateKey string oauth2SigningKey string } -func NewBuilder(getEnv EnvGetter) *Builder { - if getEnv == nil { - getEnv = os.Getenv +func NewBuilder(resolver *Resolver) *Builder { + if resolver == nil { + resolver = NewResolver(nil) } - return &Builder{getEnv: getEnv} + return &Builder{resolver: resolver} } func (b *Builder) Build() (*probodconfig.FullConfig, error) { @@ -57,272 +56,272 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { cfg := &probodconfig.FullConfig{ Unit: probodconfig.UnitConfig{ Metrics: probodconfig.MetricsConfig{ - Addr: b.getEnvOrDefault("METRICS_ADDR", "localhost:8081"), + Addr: b.resolver.getEnvOrDefault("PROBOD_METRICS_ADDR", "localhost:8081"), }, Tracing: probodconfig.TracingConfig{ - Addr: b.getEnvOrDefault("TRACING_ADDR", "localhost:4318"), - MaxBatchSize: b.getEnvIntOrDefault("TRACING_MAX_BATCH_SIZE", 512), - BatchTimeout: b.getEnvIntOrDefault("TRACING_BATCH_TIMEOUT", 5), - ExportTimeout: b.getEnvIntOrDefault("TRACING_EXPORT_TIMEOUT", 30), - MaxQueueSize: b.getEnvIntOrDefault("TRACING_MAX_QUEUE_SIZE", 2048), + Addr: b.resolver.getEnvOrDefault("PROBOD_TRACING_ADDR", "localhost:4318"), + MaxBatchSize: b.resolver.getEnvIntOrDefault("PROBOD_TRACING_MAX_BATCH_SIZE", 512), + BatchTimeout: b.resolver.getEnvIntOrDefault("PROBOD_TRACING_BATCH_TIMEOUT", 5), + ExportTimeout: b.resolver.getEnvIntOrDefault("PROBOD_TRACING_EXPORT_TIMEOUT", 30), + MaxQueueSize: b.resolver.getEnvIntOrDefault("PROBOD_TRACING_MAX_QUEUE_SIZE", 2048), }, }, Probod: probodconfig.Config{ - BaseURL: b.getEnvOrDefault("PROBOD_BASE_URL", "http://localhost:8080"), - EncryptionKey: b.getEnv("PROBOD_ENCRYPTION_KEY"), - ChromeDPAddr: b.getEnvOrDefault("CHROME_DP_ADDR", "localhost:9222"), + BaseURL: b.resolver.getEnvOrDefault("PROBOD_BASE_URL", "http://localhost:8080"), + EncryptionKey: b.resolver.getEnv("PROBOD_ENCRYPTION_KEY"), + ChromeDPAddr: b.resolver.getEnvOrDefault("PROBOD_CHROME_DP_ADDR", "localhost:9222"), Api: probodconfig.APIConfig{ - Addr: b.getEnvOrDefault("API_ADDR", ":8080"), + Addr: b.resolver.getEnvOrDefault("PROBOD_API_ADDR", ":8080"), ProxyProtocol: probodconfig.ProxyProtocolConfig{ - TrustedProxies: b.parseOriginsList(b.getEnv("API_PROXY_PROTOCOL_TRUSTED_PROXIES")), + TrustedProxies: b.parseOriginsList(b.resolver.getEnv("PROBOD_API_PROXY_PROTOCOL_TRUSTED_PROXIES")), }, Cors: probodconfig.CorsConfig{ - AllowedOrigins: b.parseOriginsList(b.getEnvOrDefault("API_CORS_ALLOWED_ORIGINS", "http://localhost:8080")), + AllowedOrigins: b.parseOriginsList(b.resolver.getEnvOrDefault("PROBOD_API_CORS_ALLOWED_ORIGINS", "http://localhost:8080")), }, ExtraHeaderFields: make(map[string]string), }, Pg: probodconfig.PgConfig{ - Addr: b.getEnvOrDefault("PG_ADDR", "localhost:5432"), - Username: b.getEnvOrDefault("PG_USERNAME", "probod"), - Password: b.getEnvOrDefault("PG_PASSWORD", "probod"), - Database: b.getEnvOrDefault("PG_DATABASE", "probod"), - PoolSize: int32(b.getEnvIntOrDefault("PG_POOL_SIZE", 100)), - MinPoolSize: int32(b.getEnvIntOrDefault("PG_MIN_POOL_SIZE", 10)), - MaxConnIdleTimeSeconds: b.getEnvIntOrDefault("PG_MAX_CONN_IDLE_TIME_SECONDS", 1800), - MaxConnLifetimeSeconds: b.getEnvIntOrDefault("PG_MAX_CONN_LIFETIME_SECONDS", 3600), - MaxConnLifetimeJitterSeconds: b.getEnvIntOrDefault("PG_MAX_CONN_LIFETIME_JITTER_SECONDS", 300), - HealthCheckPeriodSeconds: b.getEnvIntOrDefault("PG_HEALTH_CHECK_PERIOD_SECONDS", 60), + Addr: b.resolver.getEnvOrDefault("PROBOD_PG_ADDR", "localhost:5432"), + Username: b.resolver.getEnvOrDefault("PROBOD_PG_USERNAME", "probod"), + Password: b.resolver.getEnvOrDefault("PROBOD_PG_PASSWORD", "probod"), + Database: b.resolver.getEnvOrDefault("PROBOD_PG_DATABASE", "probod"), + PoolSize: int32(b.resolver.getEnvIntOrDefault("PROBOD_PG_POOL_SIZE", 100)), + MinPoolSize: int32(b.resolver.getEnvIntOrDefault("PROBOD_PG_MIN_POOL_SIZE", 10)), + MaxConnIdleTimeSeconds: b.resolver.getEnvIntOrDefault("PROBOD_PG_MAX_CONN_IDLE_TIME_SECONDS", 1800), + MaxConnLifetimeSeconds: b.resolver.getEnvIntOrDefault("PROBOD_PG_MAX_CONN_LIFETIME_SECONDS", 3600), + MaxConnLifetimeJitterSeconds: b.resolver.getEnvIntOrDefault("PROBOD_PG_MAX_CONN_LIFETIME_JITTER_SECONDS", 300), + HealthCheckPeriodSeconds: b.resolver.getEnvIntOrDefault("PROBOD_PG_HEALTH_CHECK_PERIOD_SECONDS", 60), CACertBundle: pgCACertBundle, - Debug: b.getEnvBoolOrDefault("PG_DEBUG", false), + Debug: b.resolver.getEnvBoolOrDefault("PROBOD_PG_DEBUG", false), }, Auth: probodconfig.AuthConfig{ - DisableSignup: b.getEnvBoolOrDefault("AUTH_DISABLE_SIGNUP", false), - InvitationConfirmationTokenValidity: b.getEnvIntOrDefault("AUTH_INVITATION_TOKEN_VALIDITY", 3600), - PasswordResetTokenValidity: b.getEnvIntOrDefault("AUTH_PASSWORD_RESET_TOKEN_VALIDITY", 3600), - MagicLinkTokenValidity: b.getEnvIntOrDefault("AUTH_MAGIC_LINK_TOKEN_VALIDITY", 900), + DisableSignup: b.resolver.getEnvBoolOrDefault("PROBOD_AUTH_DISABLE_SIGNUP", false), + InvitationConfirmationTokenValidity: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_INVITATION_TOKEN_VALIDITY", 3600), + PasswordResetTokenValidity: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_PASSWORD_RESET_TOKEN_VALIDITY", 3600), + MagicLinkTokenValidity: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_MAGIC_LINK_TOKEN_VALIDITY", 900), Cookie: probodconfig.CookieConfig{ - Name: b.getEnvOrDefault("AUTH_COOKIE_NAME", "SSID"), - Domain: b.getEnvOrDefault("AUTH_COOKIE_DOMAIN", "localhost"), - Secret: b.getEnv("AUTH_COOKIE_SECRET"), - Duration: b.getEnvIntOrDefault("AUTH_COOKIE_DURATION", 24), - Secure: b.getEnvBoolOrDefault("AUTH_COOKIE_SECURE", true), + Name: b.resolver.getEnvOrDefault("PROBOD_AUTH_COOKIE_NAME", "SSID"), + Domain: b.resolver.getEnvOrDefault("PROBOD_AUTH_COOKIE_DOMAIN", "localhost"), + Secret: b.resolver.getEnv("PROBOD_AUTH_COOKIE_SECRET"), + Duration: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_COOKIE_DURATION", 24), + Secure: b.resolver.getEnvBoolOrDefault("PROBOD_AUTH_COOKIE_SECURE", true), }, Password: probodconfig.PasswordConfig{ - Pepper: b.getEnv("AUTH_PASSWORD_PEPPER"), - Iterations: b.getEnvIntOrDefault("AUTH_PASSWORD_ITERATIONS", 1000000), + Pepper: b.resolver.getEnv("PROBOD_AUTH_PASSWORD_PEPPER"), + Iterations: b.resolver.getEnvIntOrDefault("PROBOD_AUTH_PASSWORD_ITERATIONS", 1000000), }, SAML: probodconfig.SAMLConfig{ - SessionDuration: b.getEnvIntOrDefault("SAML_SESSION_DURATION", 604800), - CleanupIntervalSeconds: b.getEnvIntOrDefault("SAML_CLEANUP_INTERVAL_SECONDS", 0), + SessionDuration: b.resolver.getEnvIntOrDefault("PROBOD_SAML_SESSION_DURATION", 604800), + CleanupIntervalSeconds: b.resolver.getEnvIntOrDefault("PROBOD_SAML_CLEANUP_INTERVAL_SECONDS", 0), Certificate: samlCert, PrivateKey: samlKey, - DomainVerificationIntervalSeconds: b.getEnvIntOrDefault("SAML_DOMAIN_VERIFICATION_INTERVAL_SECONDS", 60), - DomainVerificationResolverAddr: b.getEnvOrDefault("SAML_DOMAIN_VERIFICATION_RESOLVER_ADDR", "8.8.8.8:53"), + DomainVerificationIntervalSeconds: b.resolver.getEnvIntOrDefault("PROBOD_SAML_DOMAIN_VERIFICATION_INTERVAL_SECONDS", 60), + DomainVerificationResolverAddr: b.resolver.getEnvOrDefault("PROBOD_SAML_DOMAIN_VERIFICATION_RESOLVER_ADDR", "8.8.8.8:53"), }, Google: probodconfig.OIDCProviderConfig{ - ClientID: b.getEnv("AUTH_GOOGLE_CLIENT_ID"), - ClientSecret: b.getEnv("AUTH_GOOGLE_CLIENT_SECRET"), - Enabled: b.getEnv("AUTH_GOOGLE_CLIENT_ID") != "" && b.getEnv("AUTH_GOOGLE_CLIENT_SECRET") != "", + ClientID: b.resolver.getEnv("PROBOD_AUTH_GOOGLE_CLIENT_ID"), + ClientSecret: b.resolver.getEnv("PROBOD_AUTH_GOOGLE_CLIENT_SECRET"), + Enabled: b.resolver.getEnv("PROBOD_AUTH_GOOGLE_CLIENT_ID") != "" && b.resolver.getEnv("PROBOD_AUTH_GOOGLE_CLIENT_SECRET") != "", }, Microsoft: probodconfig.OIDCProviderConfig{ - ClientID: b.getEnv("AUTH_MICROSOFT_CLIENT_ID"), - ClientSecret: b.getEnv("AUTH_MICROSOFT_CLIENT_SECRET"), - Enabled: b.getEnv("AUTH_MICROSOFT_CLIENT_ID") != "" && b.getEnv("AUTH_MICROSOFT_CLIENT_SECRET") != "", + ClientID: b.resolver.getEnv("PROBOD_AUTH_MICROSOFT_CLIENT_ID"), + ClientSecret: b.resolver.getEnv("PROBOD_AUTH_MICROSOFT_CLIENT_SECRET"), + Enabled: b.resolver.getEnv("PROBOD_AUTH_MICROSOFT_CLIENT_ID") != "" && b.resolver.getEnv("PROBOD_AUTH_MICROSOFT_CLIENT_SECRET") != "", }, OAuth2Server: probodconfig.OAuth2ServerConfig{ SigningKeys: []probodconfig.OAuth2SigningKeyConfig{{ PrivateKey: oauth2SigningKey, - KID: b.getEnvOrDefault("OAUTH2_SERVER_SIGNING_KEY_KID", "default"), + KID: b.resolver.getEnvOrDefault("PROBOD_OAUTH2_SERVER_SIGNING_KEY_KID", "default"), Active: true, }}, - AccessTokenDuration: b.getEnvIntOrDefault("OAUTH2_SERVER_ACCESS_TOKEN_DURATION", 3600), - RefreshTokenDuration: b.getEnvIntOrDefault("OAUTH2_SERVER_REFRESH_TOKEN_DURATION", 2592000), - AuthorizationCodeDuration: b.getEnvIntOrDefault("OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION", 600), - DeviceCodeDuration: b.getEnvIntOrDefault("OAUTH2_SERVER_DEVICE_CODE_DURATION", 600), + AccessTokenDuration: b.resolver.getEnvIntOrDefault("PROBOD_OAUTH2_SERVER_ACCESS_TOKEN_DURATION", 3600), + RefreshTokenDuration: b.resolver.getEnvIntOrDefault("PROBOD_OAUTH2_SERVER_REFRESH_TOKEN_DURATION", 2592000), + AuthorizationCodeDuration: b.resolver.getEnvIntOrDefault("PROBOD_OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION", 600), + DeviceCodeDuration: b.resolver.getEnvIntOrDefault("PROBOD_OAUTH2_SERVER_DEVICE_CODE_DURATION", 600), CIMDAllowedClientIDs: b.parseOriginsList( - b.getEnv("OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS"), + b.resolver.getEnv("PROBOD_OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS"), ), }, }, TrustCenter: probodconfig.TrustCenterConfig{ - HTTPAddr: b.getEnvOrDefault("TRUST_CENTER_HTTP_ADDR", ":80"), - HTTPSAddr: b.getEnvOrDefault("TRUST_CENTER_HTTPS_ADDR", ":443"), + HTTPAddr: b.resolver.getEnvOrDefault("PROBOD_TRUST_CENTER_HTTP_ADDR", ":80"), + HTTPSAddr: b.resolver.getEnvOrDefault("PROBOD_TRUST_CENTER_HTTPS_ADDR", ":443"), ProxyProtocol: probodconfig.ProxyProtocolConfig{ - TrustedProxies: b.parseOriginsList(b.getEnv("TRUST_CENTER_PROXY_PROTOCOL_TRUSTED_PROXIES")), + TrustedProxies: b.parseOriginsList(b.resolver.getEnv("PROBOD_TRUST_CENTER_PROXY_PROTOCOL_TRUSTED_PROXIES")), }, }, AWS: probodconfig.AWSConfig{ - Region: b.getEnvOrDefault("AWS_REGION", "us-east-1"), - Bucket: b.getEnvOrDefault("AWS_BUCKET", "probod"), - AccessKeyID: b.getEnv("AWS_ACCESS_KEY_ID"), - SecretAccessKey: b.getEnv("AWS_SECRET_ACCESS_KEY"), - Endpoint: b.getEnv("AWS_ENDPOINT"), - UsePathStyle: b.getEnvBoolOrDefault("AWS_USE_PATH_STYLE", false), + Region: b.resolver.getEnvOrDefault("PROBOD_AWS_REGION", "us-east-1"), + Bucket: b.resolver.getEnvOrDefault("PROBOD_AWS_BUCKET", "probod"), + AccessKeyID: b.resolver.getEnv("PROBOD_AWS_ACCESS_KEY_ID"), + SecretAccessKey: b.resolver.getEnv("PROBOD_AWS_SECRET_ACCESS_KEY"), + Endpoint: b.resolver.getEnv("PROBOD_AWS_ENDPOINT"), + UsePathStyle: b.resolver.getEnvBoolOrDefault("PROBOD_AWS_USE_PATH_STYLE", false), }, Notifications: probodconfig.NotificationsConfig{ Mailer: probodconfig.MailerConfig{ - SenderName: b.getEnvOrDefault("MAILER_SENDER_NAME", "Probo"), - SenderEmail: b.getEnvOrDefault("MAILER_SENDER_EMAIL", "no-reply@notification.getprobo.com"), - MailerInterval: b.getEnvIntOrDefault("MAILER_INTERVAL", 60), + SenderName: b.resolver.getEnvOrDefault("PROBOD_MAILER_SENDER_NAME", "Probo"), + SenderEmail: b.resolver.getEnvOrDefault("PROBOD_MAILER_SENDER_EMAIL", "no-reply@notification.getprobo.com"), + MailerInterval: b.resolver.getEnvIntOrDefault("PROBOD_MAILER_INTERVAL", 60), SMTP: probodconfig.SMTPConfig{ - Addr: b.getEnvOrDefault("SMTP_ADDR", "localhost:1025"), - User: b.getEnv("SMTP_USER"), - Password: b.getEnv("SMTP_PASSWORD"), - TLSRequired: b.getEnvBoolOrDefault("SMTP_TLS_REQUIRED", false), - HelloName: b.getEnv("SMTP_HELLO_NAME"), + Addr: b.resolver.getEnvOrDefault("PROBOD_SMTP_ADDR", "localhost:1025"), + User: b.resolver.getEnv("PROBOD_SMTP_USER"), + Password: b.resolver.getEnv("PROBOD_SMTP_PASSWORD"), + TLSRequired: b.resolver.getEnvBoolOrDefault("PROBOD_SMTP_TLS_REQUIRED", false), + HelloName: b.resolver.getEnv("PROBOD_SMTP_HELLO_NAME"), }, }, Slack: probodconfig.SlackConfig{ - SenderInterval: b.getEnvIntOrDefault("SLACK_SENDER_INTERVAL", 60), - SigningSecret: b.getEnv("CONNECTOR_SLACK_SIGNING_SECRET"), + SenderInterval: b.resolver.getEnvIntOrDefault("PROBOD_SLACK_SENDER_INTERVAL", 60), + SigningSecret: b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"), }, Webhook: probodconfig.WebhookConfig{ - SenderInterval: b.getEnvIntOrDefault("WEBHOOK_SENDER_INTERVAL", 5), - CacheTTL: b.getEnvIntOrDefault("WEBHOOK_CACHE_TTL", 86400), + SenderInterval: b.resolver.getEnvIntOrDefault("PROBOD_WEBHOOK_SENDER_INTERVAL", 5), + CacheTTL: b.resolver.getEnvIntOrDefault("PROBOD_WEBHOOK_CACHE_TTL", 86400), }, Document: probodconfig.DocumentNotificationConfig{ - Interval: b.getEnvIntOrDefault("DOCUMENT_NOTIFICATION_INTERVAL", 300), - DebounceDelay: b.getEnvIntOrDefault("DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY", 900), - ReminderInterval: b.getEnvIntOrDefault("DOCUMENT_NOTIFICATION_REMINDER_INTERVAL", 86400), + Interval: b.resolver.getEnvIntOrDefault("PROBOD_DOCUMENT_NOTIFICATION_INTERVAL", 300), + DebounceDelay: b.resolver.getEnvIntOrDefault("PROBOD_DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY", 900), + ReminderInterval: b.resolver.getEnvIntOrDefault("PROBOD_DOCUMENT_NOTIFICATION_REMINDER_INTERVAL", 86400), }, }, Agents: probodconfig.AgentsConfig{ Providers: map[string]probodconfig.LLMProviderConfig{ "openai": { Type: "openai", - APIKey: b.getEnv("OPENAI_API_KEY"), + APIKey: b.resolver.getEnv("PROBOD_OPENAI_API_KEY"), }, "anthropic": { Type: "anthropic", - APIKey: b.getEnv("ANTHROPIC_API_KEY"), + APIKey: b.resolver.getEnv("PROBOD_ANTHROPIC_API_KEY"), }, }, Default: probodconfig.LLMAgentConfig{ - Provider: b.getEnvOrDefault("AGENT_DEFAULT_PROVIDER", "openai"), - ModelName: b.getEnvOrDefault("AGENT_DEFAULT_MODEL_NAME", "gpt-4o"), - Temperature: new(b.getEnvFloatOrDefault("AGENT_DEFAULT_TEMPERATURE", 0.1)), - MaxTokens: new(b.getEnvIntOrDefault("AGENT_DEFAULT_MAX_TOKENS", 4096)), + Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_DEFAULT_PROVIDER", "openai"), + ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_DEFAULT_MODEL_NAME", "gpt-4o"), + Temperature: new(b.resolver.getEnvFloatOrDefault("PROBOD_AGENT_DEFAULT_TEMPERATURE", 0.1)), + MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_DEFAULT_MAX_TOKENS", 4096)), }, Probo: probodconfig.LLMAgentConfig{ - Provider: b.getEnvOrDefault("AGENT_PROBO_PROVIDER", ""), - ModelName: b.getEnvOrDefault("AGENT_PROBO_MODEL_NAME", ""), - Temperature: b.getEnvFloatPtr("AGENT_PROBO_TEMPERATURE"), - MaxTokens: b.getEnvIntPtr("AGENT_PROBO_MAX_TOKENS"), + Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_PROBO_PROVIDER", ""), + ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_PROBO_MODEL_NAME", ""), + Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_PROBO_TEMPERATURE"), + MaxTokens: b.resolver.getEnvIntPtr("PROBOD_AGENT_PROBO_MAX_TOKENS"), }, EvidenceDescriber: probodconfig.LLMAgentConfig{ - Provider: b.getEnvOrDefault("AGENT_EVIDENCE_DESCRIBER_PROVIDER", ""), - ModelName: b.getEnvOrDefault("AGENT_EVIDENCE_DESCRIBER_MODEL_NAME", ""), - Temperature: b.getEnvFloatPtr("AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"), - MaxTokens: b.getEnvIntPtr("AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"), + Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_EVIDENCE_DESCRIBER_PROVIDER", ""), + ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_EVIDENCE_DESCRIBER_MODEL_NAME", ""), + Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"), + MaxTokens: b.resolver.getEnvIntPtr("PROBOD_AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"), }, ThirdPartyVetter: probodconfig.LLMAgentConfig{ - Provider: b.getEnvOrDefault("AGENT_THIRD_PARTY_VETTER_PROVIDER", ""), - ModelName: b.getEnvOrDefault("AGENT_THIRD_PARTY_VETTER_MODEL_NAME", ""), - Temperature: b.getEnvFloatPtr("AGENT_THIRD_PARTY_VETTER_TEMPERATURE"), - MaxTokens: b.getEnvIntPtr("AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"), + Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_THIRD_PARTY_VETTER_PROVIDER", ""), + ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_THIRD_PARTY_VETTER_MODEL_NAME", ""), + Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_THIRD_PARTY_VETTER_TEMPERATURE"), + MaxTokens: b.resolver.getEnvIntPtr("PROBOD_AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"), }, ThirdPartyDisambiguation: probodconfig.LLMAgentConfig{ - Provider: b.getEnvOrDefault("AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER", ""), - ModelName: b.getEnvOrDefault("AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME", ""), + Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER", ""), + ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME", ""), // The disambiguation agent emits a single id plus a // short rationale, but the budget must leave headroom // for reasoning models whose reasoning tokens count // against max_tokens; too small a budget truncates the // JSON. - Temperature: b.getEnvFloatPtr("AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE"), - MaxTokens: new(b.getEnvIntOrDefault("AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS", 4096)), + Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE"), + MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS", 4096)), }, TrackerMapping: probodconfig.LLMAgentConfig{ - Provider: b.getEnvOrDefault("AGENT_TRACKER_MAPPING_PROVIDER", ""), - ModelName: b.getEnvOrDefault("AGENT_TRACKER_MAPPING_MODEL_NAME", ""), + Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_TRACKER_MAPPING_PROVIDER", ""), + ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_TRACKER_MAPPING_MODEL_NAME", ""), // The tracker agents emit tiny structured JSON, but // the budget must leave headroom for reasoning // models whose reasoning tokens count against // max_tokens; too small a budget truncates the JSON. - Temperature: b.getEnvFloatPtr("AGENT_TRACKER_MAPPING_TEMPERATURE"), - MaxTokens: new(b.getEnvIntOrDefault("AGENT_TRACKER_MAPPING_MAX_TOKENS", 4096)), + Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_TRACKER_MAPPING_TEMPERATURE"), + MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_TRACKER_MAPPING_MAX_TOKENS", 4096)), }, TrackerEnrichment: probodconfig.LLMAgentConfig{ - Provider: b.getEnvOrDefault("AGENT_TRACKER_ENRICHMENT_PROVIDER", ""), - ModelName: b.getEnvOrDefault("AGENT_TRACKER_ENRICHMENT_MODEL_NAME", ""), + Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_TRACKER_ENRICHMENT_PROVIDER", ""), + ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_TRACKER_ENRICHMENT_MODEL_NAME", ""), // See the tracker-mapping note: keep ample headroom so // reasoning models do not truncate the structured JSON. - Temperature: b.getEnvFloatPtr("AGENT_TRACKER_ENRICHMENT_TEMPERATURE"), - MaxTokens: new(b.getEnvIntOrDefault("AGENT_TRACKER_ENRICHMENT_MAX_TOKENS", 4096)), + Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_TRACKER_ENRICHMENT_TEMPERATURE"), + MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_TRACKER_ENRICHMENT_MAX_TOKENS", 4096)), }, CommonThirdPartyEnrichment: probodconfig.LLMAgentConfig{ - Provider: b.getEnvOrDefault("AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER", ""), - ModelName: b.getEnvOrDefault("AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME", ""), + Provider: b.resolver.getEnvOrDefault("PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER", ""), + ModelName: b.resolver.getEnvOrDefault("PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME", ""), // Agent B browses pages and emits a moderate structured // output; the budget must leave headroom for reasoning // models whose reasoning tokens count against max_tokens. - Temperature: b.getEnvFloatPtr("AGENT_COMMON_THIRD_PARTY_ENRICHMENT_TEMPERATURE"), - MaxTokens: new(b.getEnvIntOrDefault("AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS", 8192)), + Temperature: b.resolver.getEnvFloatPtr("PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_TEMPERATURE"), + MaxTokens: new(b.resolver.getEnvIntOrDefault("PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS", 8192)), }, Tools: probodconfig.AgentToolsConfig{ - FirecrawlAPIKey: b.getEnv("FIRECRAWL_API_KEY"), + FirecrawlAPIKey: b.resolver.getEnv("PROBOD_FIRECRAWL_API_KEY"), }, }, CustomDomains: probodconfig.CustomDomainsConfig{ - RenewalInterval: b.getEnvIntOrDefault("CUSTOM_DOMAINS_RENEWAL_INTERVAL", 3600), - ProvisionInterval: b.getEnvIntOrDefault("CUSTOM_DOMAINS_PROVISION_INTERVAL", 30), - CnameTarget: b.getEnvOrDefault("CUSTOM_DOMAINS_CNAME_TARGET", "custom.getprobo.com"), - ResolverAddr: b.getEnvOrDefault("CUSTOM_DOMAINS_RESOLVER_ADDR", "8.8.8.8:53"), - CAAIssuerDomain: b.getEnvOrDefault("CUSTOM_DOMAINS_CAA_ISSUER_DOMAIN", "letsencrypt.org"), + RenewalInterval: b.resolver.getEnvIntOrDefault("PROBOD_CUSTOM_DOMAINS_RENEWAL_INTERVAL", 3600), + ProvisionInterval: b.resolver.getEnvIntOrDefault("PROBOD_CUSTOM_DOMAINS_PROVISION_INTERVAL", 30), + CnameTarget: b.resolver.getEnvOrDefault("PROBOD_CUSTOM_DOMAINS_CNAME_TARGET", "custom.getprobo.com"), + ResolverAddr: b.resolver.getEnvOrDefault("PROBOD_CUSTOM_DOMAINS_RESOLVER_ADDR", "8.8.8.8:53"), + CAAIssuerDomain: b.resolver.getEnvOrDefault("PROBOD_CUSTOM_DOMAINS_CAA_ISSUER_DOMAIN", "letsencrypt.org"), ACME: probodconfig.ACMEConfig{ - Directory: b.getEnvOrDefault("ACME_DIRECTORY", "https://acme-v02.api.letsencrypt.org/directory"), - Email: b.getEnvOrDefault("ACME_EMAIL", "admin@probo.com"), - KeyType: b.getEnvOrDefault("ACME_KEY_TYPE", "EC256"), - RootCA: b.getEnv("ACME_ROOT_CA"), - AccountKey: b.getEnv("ACME_ACCOUNT_KEY"), + Directory: b.resolver.getEnvOrDefault("PROBOD_ACME_DIRECTORY", "https://acme-v02.api.letsencrypt.org/directory"), + Email: b.resolver.getEnvOrDefault("PROBOD_ACME_EMAIL", "admin@probo.com"), + KeyType: b.resolver.getEnvOrDefault("PROBOD_ACME_KEY_TYPE", "EC256"), + RootCA: b.resolver.getEnv("PROBOD_ACME_ROOT_CA"), + AccountKey: b.resolver.getEnv("PROBOD_ACME_ACCOUNT_KEY"), }, }, SCIMBridge: probodconfig.SCIMBridgeConfig{ - SyncInterval: b.getEnvIntOrDefault("SCIM_BRIDGE_SYNC_INTERVAL", 900), - PollInterval: b.getEnvIntOrDefault("SCIM_BRIDGE_POLL_INTERVAL", 30), + SyncInterval: b.resolver.getEnvIntOrDefault("PROBOD_SCIM_BRIDGE_SYNC_INTERVAL", 900), + PollInterval: b.resolver.getEnvIntOrDefault("PROBOD_SCIM_BRIDGE_POLL_INTERVAL", 30), }, ESign: probodconfig.ESignConfig{ - TSAURL: b.getEnvOrDefault("ESIGN_TSA_URL", "http://timestamp.digicert.com"), + TSAURL: b.resolver.getEnvOrDefault("PROBOD_ESIGN_TSA_URL", "http://timestamp.digicert.com"), }, EvidenceDescriber: probodconfig.EvidenceDescriberConfig{ - Interval: b.getEnvIntOrDefault("EVIDENCE_DESCRIBER_INTERVAL", 10), - StaleAfter: b.getEnvIntOrDefault("EVIDENCE_DESCRIBER_STALE_AFTER", 300), - MaxConcurrency: b.getEnvIntOrDefault("EVIDENCE_DESCRIBER_MAX_CONCURRENCY", 10), + Interval: b.resolver.getEnvIntOrDefault("PROBOD_EVIDENCE_DESCRIBER_INTERVAL", 10), + StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_EVIDENCE_DESCRIBER_STALE_AFTER", 300), + MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_EVIDENCE_DESCRIBER_MAX_CONCURRENCY", 10), }, ThirdPartyVetting: probodconfig.ThirdPartyVettingWorkerConfig{ - Interval: b.getEnvIntOrDefault("THIRD_PARTY_VETTING_INTERVAL", 10), - StaleAfter: b.getEnvIntOrDefault("THIRD_PARTY_VETTING_STALE_AFTER", 1500), - MaxConcurrency: b.getEnvIntOrDefault("THIRD_PARTY_VETTING_MAX_CONCURRENCY", 1), + Interval: b.resolver.getEnvIntOrDefault("PROBOD_THIRD_PARTY_VETTING_INTERVAL", 10), + StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_THIRD_PARTY_VETTING_STALE_AFTER", 1500), + MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_THIRD_PARTY_VETTING_MAX_CONCURRENCY", 1), }, TrackerMappingWorker: probodconfig.TrackerMappingWorkerConfig{ - Interval: b.getEnvIntOrDefault("TRACKER_MAPPING_INTERVAL", 10), - MaxConcurrency: b.getEnvIntOrDefault("TRACKER_MAPPING_MAX_CONCURRENCY", 3), - StaleAfter: b.getEnvIntOrDefault("TRACKER_MAPPING_STALE_AFTER", 600), - AgentTimeout: b.getEnvIntOrDefault("TRACKER_MAPPING_AGENT_TIMEOUT", 45), - AgentMaxTurns: b.getEnvIntOrDefault("TRACKER_MAPPING_AGENT_MAX_TURNS", 10), - DisambiguationAgentTimeout: b.getEnvIntOrDefault("TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT", 45), + Interval: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_INTERVAL", 10), + MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_MAX_CONCURRENCY", 3), + StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_STALE_AFTER", 600), + AgentTimeout: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_AGENT_TIMEOUT", 45), + AgentMaxTurns: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_AGENT_MAX_TURNS", 10), + DisambiguationAgentTimeout: b.resolver.getEnvIntOrDefault("PROBOD_TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT", 45), }, CommonPatternEnrichmentWorker: probodconfig.CommonPatternEnrichmentWorkerConfig{ - Interval: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_INTERVAL", 10), - MaxConcurrency: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY", 2), - StaleAfter: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_STALE_AFTER", 600), - AgentTimeout: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT", 45), - AgentMaxTurns: b.getEnvIntOrDefault("COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS", 10), + Interval: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_INTERVAL", 10), + MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY", 2), + StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_STALE_AFTER", 600), + AgentTimeout: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT", 45), + AgentMaxTurns: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS", 10), }, CommonThirdPartyEnrichmentWorker: probodconfig.CommonThirdPartyEnrichmentWorkerConfig{ - Interval: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL", 10), - MaxConcurrency: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY", 1), - StaleAfter: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER", 900), - AgentTimeout: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT", 90), - AgentMaxTurns: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS", 12), - ConfidenceThreshold: b.getEnvFloatOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD", 0.7), - MaxAttempts: b.getEnvIntOrDefault("COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS", 3), + Interval: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL", 10), + MaxConcurrency: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY", 1), + StaleAfter: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER", 900), + AgentTimeout: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT", 90), + AgentMaxTurns: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS", 12), + ConfidenceThreshold: b.resolver.getEnvFloatOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD", 0.7), + MaxAttempts: b.resolver.getEnvIntOrDefault("PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS", 3), }, - Branding: b.getEnvBoolOrDefault("BRANDING", true), + Branding: b.resolver.getEnvBoolOrDefault("PROBOD_BRANDING", true), }, } - if slackClientID := b.getEnv("CONNECTOR_SLACK_CLIENT_ID"); slackClientID != "" { + if slackClientID := b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_CLIENT_ID"); slackClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -330,16 +329,16 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: slackClientID, - ClientSecret: b.getEnv("CONNECTOR_SLACK_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_CLIENT_SECRET"), }, RawSettings: map[string]any{ - "signing-secret": b.getEnv("CONNECTOR_SLACK_SIGNING_SECRET"), + "signing-secret": b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"), }, }, ) } - if hubspotClientID := b.getEnv("CONNECTOR_HUBSPOT_CLIENT_ID"); hubspotClientID != "" { + if hubspotClientID := b.resolver.getEnv("PROBOD_CONNECTOR_HUBSPOT_CLIENT_ID"); hubspotClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -347,13 +346,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: hubspotClientID, - ClientSecret: b.getEnv("CONNECTOR_HUBSPOT_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_HUBSPOT_CLIENT_SECRET"), }, }, ) } - if docusignClientID := b.getEnv("CONNECTOR_DOCUSIGN_CLIENT_ID"); docusignClientID != "" { + if docusignClientID := b.resolver.getEnv("PROBOD_CONNECTOR_DOCUSIGN_CLIENT_ID"); docusignClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -361,13 +360,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: docusignClientID, - ClientSecret: b.getEnv("CONNECTOR_DOCUSIGN_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_DOCUSIGN_CLIENT_SECRET"), }, }, ) } - if notionClientID := b.getEnv("CONNECTOR_NOTION_CLIENT_ID"); notionClientID != "" { + if notionClientID := b.resolver.getEnv("PROBOD_CONNECTOR_NOTION_CLIENT_ID"); notionClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -375,13 +374,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: notionClientID, - ClientSecret: b.getEnv("CONNECTOR_NOTION_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_NOTION_CLIENT_SECRET"), }, }, ) } - if githubClientID := b.getEnv("CONNECTOR_GITHUB_CLIENT_ID"); githubClientID != "" { + if githubClientID := b.resolver.getEnv("PROBOD_CONNECTOR_GITHUB_CLIENT_ID"); githubClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -389,13 +388,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: githubClientID, - ClientSecret: b.getEnv("CONNECTOR_GITHUB_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_GITHUB_CLIENT_SECRET"), }, }, ) } - if sentryClientID := b.getEnv("CONNECTOR_SENTRY_CLIENT_ID"); sentryClientID != "" { + if sentryClientID := b.resolver.getEnv("PROBOD_CONNECTOR_SENTRY_CLIENT_ID"); sentryClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -403,13 +402,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: sentryClientID, - ClientSecret: b.getEnv("CONNECTOR_SENTRY_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_SENTRY_CLIENT_SECRET"), }, }, ) } - if intercomClientID := b.getEnv("CONNECTOR_INTERCOM_CLIENT_ID"); intercomClientID != "" { + if intercomClientID := b.resolver.getEnv("PROBOD_CONNECTOR_INTERCOM_CLIENT_ID"); intercomClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -417,13 +416,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: intercomClientID, - ClientSecret: b.getEnv("CONNECTOR_INTERCOM_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_INTERCOM_CLIENT_SECRET"), }, }, ) } - if brexClientID := b.getEnv("CONNECTOR_BREX_CLIENT_ID"); brexClientID != "" { + if brexClientID := b.resolver.getEnv("PROBOD_CONNECTOR_BREX_CLIENT_ID"); brexClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -431,13 +430,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: brexClientID, - ClientSecret: b.getEnv("CONNECTOR_BREX_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_BREX_CLIENT_SECRET"), }, }, ) } - if googleWorkspaceClientID := b.getEnv("CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID"); googleWorkspaceClientID != "" { + if googleWorkspaceClientID := b.resolver.getEnv("PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID"); googleWorkspaceClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -445,13 +444,13 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: googleWorkspaceClientID, - ClientSecret: b.getEnv("CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"), }, }, ) } - if microsoft365ClientID := b.getEnv("CONNECTOR_MICROSOFT_365_CLIENT_ID"); microsoft365ClientID != "" { + if microsoft365ClientID := b.resolver.getEnv("PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_ID"); microsoft365ClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -459,7 +458,7 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: microsoft365ClientID, - ClientSecret: b.getEnv("CONNECTOR_MICROSOFT_365_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_SECRET"), }, }, ) @@ -476,8 +475,9 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { "MONDAY", "DATADOG", "ZENDESK", + "LINEAR", } { - clientID := b.getEnv("CONNECTOR_" + provider + "_CLIENT_ID") + clientID := b.resolver.getEnv("PROBOD_CONNECTOR_" + provider + "_CLIENT_ID") if clientID == "" { continue } @@ -489,7 +489,7 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: clientID, - ClientSecret: b.getEnv("CONNECTOR_" + provider + "_CLIENT_SECRET"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_" + provider + "_CLIENT_SECRET"), }, }, ) @@ -497,7 +497,7 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { // Vercel needs the operator-supplied integration slug to resolve the // templated AuthURL ("https://vercel.com/integrations/{integration_slug}/new"). - if vercelClientID := b.getEnv("CONNECTOR_VERCEL_CLIENT_ID"); vercelClientID != "" { + if vercelClientID := b.resolver.getEnv("PROBOD_CONNECTOR_VERCEL_CLIENT_ID"); vercelClientID != "" { cfg.Probod.Connectors = append( cfg.Probod.Connectors, probodconfig.ConnectorConfig{ @@ -505,13 +505,17 @@ func (b *Builder) Build() (*probodconfig.FullConfig, error) { Protocol: "oauth2", RawConfig: probodconfig.ConnectorConfigOAuth2{ ClientID: vercelClientID, - ClientSecret: b.getEnv("CONNECTOR_VERCEL_CLIENT_SECRET"), - IntegrationSlug: b.getEnv("CONNECTOR_VERCEL_INTEGRATION_SLUG"), + ClientSecret: b.resolver.getEnv("PROBOD_CONNECTOR_VERCEL_CLIENT_SECRET"), + IntegrationSlug: b.resolver.getEnv("PROBOD_CONNECTOR_VERCEL_INTEGRATION_SLUG"), }, }, ) } + if b.resolver.Err() != nil { + return nil, b.resolver.Err() + } + return cfg, nil } @@ -520,28 +524,28 @@ func (b *Builder) validateRequired() error { required := []string{ "PROBOD_ENCRYPTION_KEY", - "AUTH_COOKIE_SECRET", - "AUTH_PASSWORD_PEPPER", + "PROBOD_AUTH_COOKIE_SECRET", + "PROBOD_AUTH_PASSWORD_PEPPER", } for _, key := range required { - if b.getEnv(key) == "" { + if b.resolver.getEnv(key) == "" { missing = append(missing, key) } } - if b.oauth2SigningKey == "" && b.getEnv("OAUTH2_SERVER_SIGNING_KEY") == "" { - missing = append(missing, "OAUTH2_SERVER_SIGNING_KEY") + if b.oauth2SigningKey == "" && b.resolver.getEnv("PROBOD_OAUTH2_SERVER_SIGNING_KEY") == "" { + missing = append(missing, "PROBOD_OAUTH2_SERVER_SIGNING_KEY") } - if slackClientID := b.getEnv("CONNECTOR_SLACK_CLIENT_ID"); slackClientID != "" { + if slackClientID := b.resolver.getEnv("PROBOD_CONNECTOR_SLACK_CLIENT_ID"); slackClientID != "" { slackRequired := []string{ - "CONNECTOR_SLACK_CLIENT_SECRET", - "CONNECTOR_SLACK_SIGNING_SECRET", + "PROBOD_CONNECTOR_SLACK_CLIENT_SECRET", + "PROBOD_CONNECTOR_SLACK_SIGNING_SECRET", } for _, key := range slackRequired { - if b.getEnv(key) == "" { - missing = append(missing, key+" (required when CONNECTOR_SLACK_CLIENT_ID is set)") + if b.resolver.getEnv(key) == "" { + missing = append(missing, key+" (required when PROBOD_CONNECTOR_SLACK_CLIENT_ID is set)") } } } @@ -569,15 +573,16 @@ func (b *Builder) validateRequired() error { {"CONNECTOR_MONDAY", []string{"CLIENT_SECRET"}}, {"CONNECTOR_DATADOG", []string{"CLIENT_SECRET"}}, {"CONNECTOR_ZENDESK", []string{"CLIENT_SECRET"}}, + {"CONNECTOR_LINEAR", []string{"CLIENT_SECRET"}}, {"CONNECTOR_VERCEL", []string{"CLIENT_SECRET", "INTEGRATION_SLUG"}}, } for _, p := range oauthProviders { - clientIDKey := p.envPrefix + "_CLIENT_ID" - if b.getEnv(clientIDKey) != "" { + clientIDKey := "PROBOD_" + p.envPrefix + "_CLIENT_ID" + if b.resolver.getEnv(clientIDKey) != "" { for _, suffix := range p.required { - key := p.envPrefix + "_" + suffix - if b.getEnv(key) == "" { + key := "PROBOD_" + p.envPrefix + "_" + suffix + if b.resolver.getEnv(key) == "" { missing = append(missing, key+" (required when "+clientIDKey+" is set)") } } @@ -585,6 +590,10 @@ func (b *Builder) validateRequired() error { } if len(missing) > 0 { + if err := b.resolver.Err(); err != nil { + return err + } + return fmt.Errorf("missing required environment variables:\n - %s", strings.Join(missing, "\n - ")) } @@ -596,11 +605,11 @@ func (b *Builder) getSAMLCredentials() (cert, key string, err error) { key = b.samlPrivateKey if cert == "" { - cert = b.getEnv("SAML_CERTIFICATE") + cert = b.resolver.getEnv("PROBOD_SAML_CERTIFICATE") } if key == "" { - key = b.getEnv("SAML_PRIVATE_KEY") + key = b.resolver.getEnv("PROBOD_SAML_PRIVATE_KEY") } if cert == "" || key == "" { @@ -618,77 +627,18 @@ func (b *Builder) getOAuth2SigningKey() string { return b.oauth2SigningKey } - return b.getEnv("OAUTH2_SERVER_SIGNING_KEY") + return b.resolver.getEnv("PROBOD_OAUTH2_SERVER_SIGNING_KEY") } func (b *Builder) getPgCACertBundle() string { - if path := b.getEnv("PG_CA_BUNDLE_PATH"); path != "" { + if path := b.resolver.getEnv("PROBOD_PG_CA_BUNDLE_PATH"); path != "" { data, err := os.ReadFile(path) if err == nil { return string(data) } } - return b.getEnv("PG_CA_BUNDLE") -} - -func (b *Builder) getEnvOrDefault(key, defaultValue string) string { - if value := b.getEnv(key); value != "" { - return value - } - - return defaultValue -} - -func (b *Builder) getEnvIntOrDefault(key string, defaultValue int) int { - if value := b.getEnv(key); value != "" { - if intValue, err := strconv.ParseInt(value, 10, 32); err == nil { - return int(intValue) - } - } - - return defaultValue -} - -func (b *Builder) getEnvFloatOrDefault(key string, defaultValue float64) float64 { - if value := b.getEnv(key); value != "" { - if floatValue, err := strconv.ParseFloat(value, 64); err == nil { - return floatValue - } - } - - return defaultValue -} - -func (b *Builder) getEnvFloatPtr(key string) *float64 { - if value := b.getEnv(key); value != "" { - if floatValue, err := strconv.ParseFloat(value, 64); err == nil { - return &floatValue - } - } - - return nil -} - -func (b *Builder) getEnvIntPtr(key string) *int { - if value := b.getEnv(key); value != "" { - if intValue, err := strconv.ParseInt(value, 10, 32); err == nil { - v := int(intValue) - return &v - } - } - - return nil -} - -func (b *Builder) getEnvBoolOrDefault(key string, defaultValue bool) bool { - if value := b.getEnv(key); value != "" { - if boolValue, err := strconv.ParseBool(value); err == nil { - return boolValue - } - } - - return defaultValue + return b.resolver.getEnv("PROBOD_PG_CA_BUNDLE") } func (b *Builder) parseOriginsList(s string) []string { diff --git a/pkg/bootstrap/builder_test.go b/pkg/bootstrap/builder_test.go index 94f35efce..850938076 100644 --- a/pkg/bootstrap/builder_test.go +++ b/pkg/bootstrap/builder_test.go @@ -33,10 +33,10 @@ func mockEnv(env map[string]string) EnvGetter { func requiredEnv() map[string]string { return map[string]string{ - "PROBOD_ENCRYPTION_KEY": "test-encryption-key-32-bytes-long", - "AUTH_COOKIE_SECRET": "test-cookie-secret-32-bytes-long!", - "AUTH_PASSWORD_PEPPER": "test-password-pepper-32-bytes-lo", - "OAUTH2_SERVER_SIGNING_KEY": "test-oauth2-signing-key", + "PROBOD_ENCRYPTION_KEY": "test-encryption-key-32-bytes-long", + "PROBOD_AUTH_COOKIE_SECRET": "test-cookie-secret-32-bytes-long!", + "PROBOD_AUTH_PASSWORD_PEPPER": "test-password-pepper-32-bytes-lo", + "PROBOD_OAUTH2_SERVER_SIGNING_KEY": "test-oauth2-signing-key", } } @@ -49,68 +49,68 @@ func TestBuilder_Build_MissingRequiredEnvVars(t *testing.T) { { name: "all missing", env: map[string]string{}, - wantMissing: []string{"PROBOD_ENCRYPTION_KEY", "AUTH_COOKIE_SECRET", "AUTH_PASSWORD_PEPPER", "OAUTH2_SERVER_SIGNING_KEY"}, + wantMissing: []string{"PROBOD_ENCRYPTION_KEY", "PROBOD_AUTH_COOKIE_SECRET", "PROBOD_AUTH_PASSWORD_PEPPER", "PROBOD_OAUTH2_SERVER_SIGNING_KEY"}, }, { name: "missing oauth2 signing key", env: map[string]string{ - "PROBOD_ENCRYPTION_KEY": "key", - "AUTH_COOKIE_SECRET": "secret", - "AUTH_PASSWORD_PEPPER": "pepper", + "PROBOD_ENCRYPTION_KEY": "key", + "PROBOD_AUTH_COOKIE_SECRET": "secret", + "PROBOD_AUTH_PASSWORD_PEPPER": "pepper", }, - wantMissing: []string{"OAUTH2_SERVER_SIGNING_KEY"}, + wantMissing: []string{"PROBOD_OAUTH2_SERVER_SIGNING_KEY"}, }, { name: "missing encryption key", env: map[string]string{ - "AUTH_COOKIE_SECRET": "secret", - "AUTH_PASSWORD_PEPPER": "pepper", + "PROBOD_AUTH_COOKIE_SECRET": "secret", + "PROBOD_AUTH_PASSWORD_PEPPER": "pepper", }, wantMissing: []string{"PROBOD_ENCRYPTION_KEY"}, }, { name: "missing cookie secret", env: map[string]string{ - "PROBOD_ENCRYPTION_KEY": "key", - "AUTH_PASSWORD_PEPPER": "pepper", + "PROBOD_ENCRYPTION_KEY": "key", + "PROBOD_AUTH_PASSWORD_PEPPER": "pepper", }, - wantMissing: []string{"AUTH_COOKIE_SECRET"}, + wantMissing: []string{"PROBOD_AUTH_COOKIE_SECRET"}, }, { name: "slack connector missing required fields", env: map[string]string{ - "PROBOD_ENCRYPTION_KEY": "key", - "AUTH_COOKIE_SECRET": "secret", - "AUTH_PASSWORD_PEPPER": "pepper", - "CONNECTOR_SLACK_CLIENT_ID": "client-id", + "PROBOD_ENCRYPTION_KEY": "key", + "PROBOD_AUTH_COOKIE_SECRET": "secret", + "PROBOD_AUTH_PASSWORD_PEPPER": "pepper", + "PROBOD_CONNECTOR_SLACK_CLIENT_ID": "client-id", }, - wantMissing: []string{"CONNECTOR_SLACK_CLIENT_SECRET", "CONNECTOR_SLACK_SIGNING_SECRET"}, + wantMissing: []string{"PROBOD_CONNECTOR_SLACK_CLIENT_SECRET", "PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"}, }, { name: "google workspace connector missing required fields", env: map[string]string{ - "PROBOD_ENCRYPTION_KEY": "key", - "AUTH_COOKIE_SECRET": "secret", - "AUTH_PASSWORD_PEPPER": "pepper", - "CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID": "client-id", + "PROBOD_ENCRYPTION_KEY": "key", + "PROBOD_AUTH_COOKIE_SECRET": "secret", + "PROBOD_AUTH_PASSWORD_PEPPER": "pepper", + "PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID": "client-id", }, - wantMissing: []string{"CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"}, + wantMissing: []string{"PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"}, }, { name: "microsoft 365 connector missing required fields", env: map[string]string{ - "PROBOD_ENCRYPTION_KEY": "key", - "AUTH_COOKIE_SECRET": "secret", - "AUTH_PASSWORD_PEPPER": "pepper", - "CONNECTOR_MICROSOFT_365_CLIENT_ID": "client-id", + "PROBOD_ENCRYPTION_KEY": "key", + "PROBOD_AUTH_COOKIE_SECRET": "secret", + "PROBOD_AUTH_PASSWORD_PEPPER": "pepper", + "PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_ID": "client-id", }, - wantMissing: []string{"CONNECTOR_MICROSOFT_365_CLIENT_SECRET"}, + wantMissing: []string{"PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_SECRET"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - b := NewBuilder(mockEnv(tt.env)) + b := NewBuilder(NewResolver(mockEnv(tt.env))) _, err := b.Build() require.Error(t, err) @@ -123,7 +123,7 @@ func TestBuilder_Build_MissingRequiredEnvVars(t *testing.T) { } func TestBuilder_Build_Defaults(t *testing.T) { - b := NewBuilder(mockEnv(requiredEnv())) + b := NewBuilder(NewResolver(mockEnv(requiredEnv()))) b.samlCertificate = "test-cert" b.samlPrivateKey = "test-key" @@ -285,129 +285,129 @@ func TestBuilder_Build_Defaults(t *testing.T) { func TestBuilder_Build_CustomValues(t *testing.T) { env := requiredEnv() // Unit - env["METRICS_ADDR"] = "0.0.0.0:9090" - env["TRACING_ADDR"] = "jaeger:4317" - env["TRACING_MAX_BATCH_SIZE"] = "1024" + env["PROBOD_METRICS_ADDR"] = "0.0.0.0:9090" + env["PROBOD_TRACING_ADDR"] = "jaeger:4317" + env["PROBOD_TRACING_MAX_BATCH_SIZE"] = "1024" // Probod env["PROBOD_BASE_URL"] = "https://app.example.com" - env["CHROME_DP_ADDR"] = "chrome:9222" + env["PROBOD_CHROME_DP_ADDR"] = "chrome:9222" // API - env["API_ADDR"] = "0.0.0.0:8080" - env["API_CORS_ALLOWED_ORIGINS"] = "https://app.example.com,https://admin.example.com" - env["API_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.0.1,10.0.0.2" + env["PROBOD_API_ADDR"] = "0.0.0.0:8080" + env["PROBOD_API_CORS_ALLOWED_ORIGINS"] = "https://app.example.com,https://admin.example.com" + env["PROBOD_API_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.0.1,10.0.0.2" // PG - env["PG_ADDR"] = "postgres.example.com:5432" - env["PG_USERNAME"] = "probo" - env["PG_PASSWORD"] = "secret123" - env["PG_DATABASE"] = "probo_prod" - env["PG_POOL_SIZE"] = "200" - env["PG_MIN_POOL_SIZE"] = "25" - env["PG_MAX_CONN_IDLE_TIME_SECONDS"] = "900" - env["PG_MAX_CONN_LIFETIME_SECONDS"] = "7200" - env["PG_MAX_CONN_LIFETIME_JITTER_SECONDS"] = "600" - env["PG_HEALTH_CHECK_PERIOD_SECONDS"] = "30" - env["PG_DEBUG"] = "true" + env["PROBOD_PG_ADDR"] = "postgres.example.com:5432" + env["PROBOD_PG_USERNAME"] = "probo" + env["PROBOD_PG_PASSWORD"] = "secret123" + env["PROBOD_PG_DATABASE"] = "probo_prod" + env["PROBOD_PG_POOL_SIZE"] = "200" + env["PROBOD_PG_MIN_POOL_SIZE"] = "25" + env["PROBOD_PG_MAX_CONN_IDLE_TIME_SECONDS"] = "900" + env["PROBOD_PG_MAX_CONN_LIFETIME_SECONDS"] = "7200" + env["PROBOD_PG_MAX_CONN_LIFETIME_JITTER_SECONDS"] = "600" + env["PROBOD_PG_HEALTH_CHECK_PERIOD_SECONDS"] = "30" + env["PROBOD_PG_DEBUG"] = "true" // Auth - env["AUTH_DISABLE_SIGNUP"] = "true" - env["AUTH_INVITATION_TOKEN_VALIDITY"] = "7200" - env["AUTH_PASSWORD_RESET_TOKEN_VALIDITY"] = "1800" - env["AUTH_MAGIC_LINK_TOKEN_VALIDITY"] = "600" - env["AUTH_COOKIE_DOMAIN"] = ".example.com" - env["AUTH_COOKIE_DURATION"] = "48" + env["PROBOD_AUTH_DISABLE_SIGNUP"] = "true" + env["PROBOD_AUTH_INVITATION_TOKEN_VALIDITY"] = "7200" + env["PROBOD_AUTH_PASSWORD_RESET_TOKEN_VALIDITY"] = "1800" + env["PROBOD_AUTH_MAGIC_LINK_TOKEN_VALIDITY"] = "600" + env["PROBOD_AUTH_COOKIE_DOMAIN"] = ".example.com" + env["PROBOD_AUTH_COOKIE_DURATION"] = "48" // SAML - env["SAML_DOMAIN_VERIFICATION_INTERVAL_SECONDS"] = "120" - env["SAML_DOMAIN_VERIFICATION_RESOLVER_ADDR"] = "1.1.1.1:53" + env["PROBOD_SAML_DOMAIN_VERIFICATION_INTERVAL_SECONDS"] = "120" + env["PROBOD_SAML_DOMAIN_VERIFICATION_RESOLVER_ADDR"] = "1.1.1.1:53" // Trust center - env["TRUST_CENTER_HTTP_ADDR"] = ":8080" - env["TRUST_CENTER_HTTPS_ADDR"] = ":8443" - env["TRUST_CENTER_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.1.1,10.0.1.2" + env["PROBOD_TRUST_CENTER_HTTP_ADDR"] = ":8080" + env["PROBOD_TRUST_CENTER_HTTPS_ADDR"] = ":8443" + env["PROBOD_TRUST_CENTER_PROXY_PROTOCOL_TRUSTED_PROXIES"] = "10.0.1.1,10.0.1.2" // AWS - env["AWS_REGION"] = "eu-west-1" - env["AWS_BUCKET"] = "probo-files" - env["AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE" - env["AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" - env["AWS_ENDPOINT"] = "https://s3.example.com" - env["AWS_USE_PATH_STYLE"] = "true" + env["PROBOD_AWS_REGION"] = "eu-west-1" + env["PROBOD_AWS_BUCKET"] = "probo-files" + env["PROBOD_AWS_ACCESS_KEY_ID"] = "AKIAIOSFODNN7EXAMPLE" + env["PROBOD_AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + env["PROBOD_AWS_ENDPOINT"] = "https://s3.example.com" + env["PROBOD_AWS_USE_PATH_STYLE"] = "true" // Notifications - env["WEBHOOK_SENDER_INTERVAL"] = "10" - env["WEBHOOK_CACHE_TTL"] = "3600" - env["CONNECTOR_SLACK_SIGNING_SECRET"] = "slack-signing-secret" - env["DOCUMENT_NOTIFICATION_INTERVAL"] = "120" - env["DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY"] = "60" - env["DOCUMENT_NOTIFICATION_REMINDER_INTERVAL"] = "43200" + env["PROBOD_WEBHOOK_SENDER_INTERVAL"] = "10" + env["PROBOD_WEBHOOK_CACHE_TTL"] = "3600" + env["PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"] = "slack-signing-secret" + env["PROBOD_DOCUMENT_NOTIFICATION_INTERVAL"] = "120" + env["PROBOD_DOCUMENT_NOTIFICATION_DEBOUNCE_DELAY"] = "60" + env["PROBOD_DOCUMENT_NOTIFICATION_REMINDER_INTERVAL"] = "43200" // Firecrawl - env["FIRECRAWL_API_KEY"] = "fc-test-key" + env["PROBOD_FIRECRAWL_API_KEY"] = "fc-test-key" // Agents — providers - env["OPENAI_API_KEY"] = "sk-test-key" - env["ANTHROPIC_API_KEY"] = "sk-ant-test-key" + env["PROBOD_OPENAI_API_KEY"] = "sk-test-key" + env["PROBOD_ANTHROPIC_API_KEY"] = "sk-ant-test-key" // Agents — default - env["AGENT_DEFAULT_PROVIDER"] = "openai" - env["AGENT_DEFAULT_MODEL_NAME"] = "gpt-4-turbo" - env["AGENT_DEFAULT_TEMPERATURE"] = "0.5" - env["AGENT_DEFAULT_MAX_TOKENS"] = "8192" + env["PROBOD_AGENT_DEFAULT_PROVIDER"] = "openai" + env["PROBOD_AGENT_DEFAULT_MODEL_NAME"] = "gpt-4-turbo" + env["PROBOD_AGENT_DEFAULT_TEMPERATURE"] = "0.5" + env["PROBOD_AGENT_DEFAULT_MAX_TOKENS"] = "8192" // Agents — evidence-describer override - env["AGENT_EVIDENCE_DESCRIBER_PROVIDER"] = "anthropic" - env["AGENT_EVIDENCE_DESCRIBER_MODEL_NAME"] = "claude-sonnet-4-20250514" - env["AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"] = "0.2" - env["AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"] = "4096" + env["PROBOD_AGENT_EVIDENCE_DESCRIBER_PROVIDER"] = "anthropic" + env["PROBOD_AGENT_EVIDENCE_DESCRIBER_MODEL_NAME"] = "claude-sonnet-4-20250514" + env["PROBOD_AGENT_EVIDENCE_DESCRIBER_TEMPERATURE"] = "0.2" + env["PROBOD_AGENT_EVIDENCE_DESCRIBER_MAX_TOKENS"] = "4096" // Agents — third-party-vetter override - env["AGENT_THIRD_PARTY_VETTER_PROVIDER"] = "openai" - env["AGENT_THIRD_PARTY_VETTER_MODEL_NAME"] = "gpt-4o" - env["AGENT_THIRD_PARTY_VETTER_TEMPERATURE"] = "0.3" - env["AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"] = "8192" + env["PROBOD_AGENT_THIRD_PARTY_VETTER_PROVIDER"] = "openai" + env["PROBOD_AGENT_THIRD_PARTY_VETTER_MODEL_NAME"] = "gpt-4o" + env["PROBOD_AGENT_THIRD_PARTY_VETTER_TEMPERATURE"] = "0.3" + env["PROBOD_AGENT_THIRD_PARTY_VETTER_MAX_TOKENS"] = "8192" // Agents — third-party-disambiguation override - env["AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER"] = "anthropic" - env["AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME"] = "claude-sonnet-4-20250514" - env["AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE"] = "0.4" - env["AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS"] = "2048" + env["PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_PROVIDER"] = "anthropic" + env["PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MODEL_NAME"] = "claude-sonnet-4-20250514" + env["PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_TEMPERATURE"] = "0.4" + env["PROBOD_AGENT_THIRD_PARTY_DISAMBIGUATION_MAX_TOKENS"] = "2048" // Agents — tracker-mapping override - env["AGENT_TRACKER_MAPPING_PROVIDER"] = "openai" - env["AGENT_TRACKER_MAPPING_MODEL_NAME"] = "gpt-4o-mini" - env["AGENT_TRACKER_MAPPING_TEMPERATURE"] = "0.1" - env["AGENT_TRACKER_MAPPING_MAX_TOKENS"] = "1024" + env["PROBOD_AGENT_TRACKER_MAPPING_PROVIDER"] = "openai" + env["PROBOD_AGENT_TRACKER_MAPPING_MODEL_NAME"] = "gpt-4o-mini" + env["PROBOD_AGENT_TRACKER_MAPPING_TEMPERATURE"] = "0.1" + env["PROBOD_AGENT_TRACKER_MAPPING_MAX_TOKENS"] = "1024" // Agents — tracker-enrichment override - env["AGENT_TRACKER_ENRICHMENT_PROVIDER"] = "openai" - env["AGENT_TRACKER_ENRICHMENT_MODEL_NAME"] = "gpt-4o" - env["AGENT_TRACKER_ENRICHMENT_TEMPERATURE"] = "0.2" - env["AGENT_TRACKER_ENRICHMENT_MAX_TOKENS"] = "2048" + env["PROBOD_AGENT_TRACKER_ENRICHMENT_PROVIDER"] = "openai" + env["PROBOD_AGENT_TRACKER_ENRICHMENT_MODEL_NAME"] = "gpt-4o" + env["PROBOD_AGENT_TRACKER_ENRICHMENT_TEMPERATURE"] = "0.2" + env["PROBOD_AGENT_TRACKER_ENRICHMENT_MAX_TOKENS"] = "2048" // Tracker worker tuning override - env["TRACKER_MAPPING_INTERVAL"] = "20" - env["TRACKER_MAPPING_MAX_CONCURRENCY"] = "5" - env["TRACKER_MAPPING_STALE_AFTER"] = "1200" - env["TRACKER_MAPPING_AGENT_TIMEOUT"] = "30" - env["TRACKER_MAPPING_AGENT_MAX_TURNS"] = "6" - env["TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT"] = "35" - env["COMMON_PATTERN_ENRICHMENT_INTERVAL"] = "15" - env["COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY"] = "4" - env["COMMON_PATTERN_ENRICHMENT_STALE_AFTER"] = "900" - env["COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT"] = "50" - env["COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS"] = "5" + env["PROBOD_TRACKER_MAPPING_INTERVAL"] = "20" + env["PROBOD_TRACKER_MAPPING_MAX_CONCURRENCY"] = "5" + env["PROBOD_TRACKER_MAPPING_STALE_AFTER"] = "1200" + env["PROBOD_TRACKER_MAPPING_AGENT_TIMEOUT"] = "30" + env["PROBOD_TRACKER_MAPPING_AGENT_MAX_TURNS"] = "6" + env["PROBOD_TRACKER_MAPPING_DISAMBIGUATION_AGENT_TIMEOUT"] = "35" + env["PROBOD_COMMON_PATTERN_ENRICHMENT_INTERVAL"] = "15" + env["PROBOD_COMMON_PATTERN_ENRICHMENT_MAX_CONCURRENCY"] = "4" + env["PROBOD_COMMON_PATTERN_ENRICHMENT_STALE_AFTER"] = "900" + env["PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_TIMEOUT"] = "50" + env["PROBOD_COMMON_PATTERN_ENRICHMENT_AGENT_MAX_TURNS"] = "5" // Common third party enrichment agent + worker tuning override - env["AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER"] = "openai" - env["AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME"] = "gpt-4o" - env["AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS"] = "16384" - env["COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL"] = "25" - env["COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY"] = "2" - env["COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER"] = "1200" - env["COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT"] = "120" - env["COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS"] = "8" - env["COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD"] = "0.85" - env["COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS"] = "5" - env["THIRD_PARTY_VETTING_INTERVAL"] = "15" - env["THIRD_PARTY_VETTING_STALE_AFTER"] = "1800" - env["THIRD_PARTY_VETTING_MAX_CONCURRENCY"] = "2" + env["PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_PROVIDER"] = "openai" + env["PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MODEL_NAME"] = "gpt-4o" + env["PROBOD_AGENT_COMMON_THIRD_PARTY_ENRICHMENT_MAX_TOKENS"] = "16384" + env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_INTERVAL"] = "25" + env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_CONCURRENCY"] = "2" + env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_STALE_AFTER"] = "1200" + env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_TIMEOUT"] = "120" + env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS"] = "8" + env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD"] = "0.85" + env["PROBOD_COMMON_THIRD_PARTY_ENRICHMENT_MAX_ATTEMPTS"] = "5" + env["PROBOD_THIRD_PARTY_VETTING_INTERVAL"] = "15" + env["PROBOD_THIRD_PARTY_VETTING_STALE_AFTER"] = "1800" + env["PROBOD_THIRD_PARTY_VETTING_MAX_CONCURRENCY"] = "2" // Custom domains - env["CUSTOM_DOMAINS_RESOLVER_ADDR"] = "1.1.1.1:53" - env["ACME_ACCOUNT_KEY"] = "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----" + env["PROBOD_CUSTOM_DOMAINS_RESOLVER_ADDR"] = "1.1.1.1:53" + env["PROBOD_ACME_ACCOUNT_KEY"] = "-----BEGIN EC PRIVATE KEY-----\ntest\n-----END EC PRIVATE KEY-----" // SCIM bridge - env["SCIM_BRIDGE_SYNC_INTERVAL"] = "1800" - env["SCIM_BRIDGE_POLL_INTERVAL"] = "60" + env["PROBOD_SCIM_BRIDGE_SYNC_INTERVAL"] = "1800" + env["PROBOD_SCIM_BRIDGE_POLL_INTERVAL"] = "60" // ESign - env["ESIGN_TSA_URL"] = "http://custom.tsa.example.com" + env["PROBOD_ESIGN_TSA_URL"] = "http://custom.tsa.example.com" // Branding - env["BRANDING"] = "false" + env["PROBOD_BRANDING"] = "false" - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) b.samlCertificate = "test-cert" b.samlPrivateKey = "test-key" @@ -545,10 +545,10 @@ func TestBuilder_Build_CustomValues(t *testing.T) { func TestBuilder_Build_GoogleWorkspaceConnector(t *testing.T) { env := requiredEnv() - env["CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID"] = "gw-client-id" - env["CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"] = "gw-client-secret" + env["PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_ID"] = "gw-client-id" + env["PROBOD_CONNECTOR_GOOGLE_WORKSPACE_CLIENT_SECRET"] = "gw-client-secret" - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) b.samlCertificate = "test-cert" b.samlPrivateKey = "test-key" @@ -566,10 +566,10 @@ func TestBuilder_Build_GoogleWorkspaceConnector(t *testing.T) { func TestBuilder_Build_Microsoft365Connector(t *testing.T) { env := requiredEnv() - env["CONNECTOR_MICROSOFT_365_CLIENT_ID"] = "ms365-client-id" - env["CONNECTOR_MICROSOFT_365_CLIENT_SECRET"] = "ms365-client-secret" + env["PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_ID"] = "ms365-client-id" + env["PROBOD_CONNECTOR_MICROSOFT_365_CLIENT_SECRET"] = "ms365-client-secret" - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) b.samlCertificate = "test-cert" b.samlPrivateKey = "test-key" @@ -592,16 +592,16 @@ func TestBuilder_Build_AccessReviewConnectors(t *testing.T) { providers := []string{ "GITLAB", "BITBUCKET", "HEROKU", "PAGERDUTY", "ASANA", "NETLIFY", "CLICKUP", "MONDAY", "DATADOG", - "ZENDESK", + "ZENDESK", "LINEAR", } env := requiredEnv() for _, provider := range providers { - env["CONNECTOR_"+provider+"_CLIENT_ID"] = strings.ToLower(provider) + "-id" - env["CONNECTOR_"+provider+"_CLIENT_SECRET"] = strings.ToLower(provider) + "-secret" + env["PROBOD_CONNECTOR_"+provider+"_CLIENT_ID"] = strings.ToLower(provider) + "-id" + env["PROBOD_CONNECTOR_"+provider+"_CLIENT_SECRET"] = strings.ToLower(provider) + "-secret" } - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) b.samlCertificate = "test-cert" b.samlPrivateKey = "test-key" @@ -628,11 +628,11 @@ func TestBuilder_Build_AccessReviewConnectors(t *testing.T) { func TestBuilder_Build_VercelConnector(t *testing.T) { env := requiredEnv() - env["CONNECTOR_VERCEL_CLIENT_ID"] = "vercel-id" - env["CONNECTOR_VERCEL_CLIENT_SECRET"] = "vercel-secret" - env["CONNECTOR_VERCEL_INTEGRATION_SLUG"] = "probo-app" + env["PROBOD_CONNECTOR_VERCEL_CLIENT_ID"] = "vercel-id" + env["PROBOD_CONNECTOR_VERCEL_CLIENT_SECRET"] = "vercel-secret" + env["PROBOD_CONNECTOR_VERCEL_INTEGRATION_SLUG"] = "probo-app" - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) b.samlCertificate = "test-cert" b.samlPrivateKey = "test-key" @@ -651,11 +651,11 @@ func TestBuilder_Build_VercelConnector(t *testing.T) { func TestBuilder_Build_SlackConnector(t *testing.T) { env := requiredEnv() - env["CONNECTOR_SLACK_CLIENT_ID"] = "slack-client-id" - env["CONNECTOR_SLACK_CLIENT_SECRET"] = "slack-client-secret" - env["CONNECTOR_SLACK_SIGNING_SECRET"] = "slack-signing-secret" + env["PROBOD_CONNECTOR_SLACK_CLIENT_ID"] = "slack-client-id" + env["PROBOD_CONNECTOR_SLACK_CLIENT_SECRET"] = "slack-client-secret" + env["PROBOD_CONNECTOR_SLACK_SIGNING_SECRET"] = "slack-signing-secret" - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) b.samlCertificate = "test-cert" b.samlPrivateKey = "test-key" @@ -675,7 +675,7 @@ func TestBuilder_Build_SlackConnector(t *testing.T) { } func TestBuilder_Build_SAMLAutoGeneration(t *testing.T) { - b := NewBuilder(mockEnv(requiredEnv())) + b := NewBuilder(NewResolver(mockEnv(requiredEnv()))) cfg, err := b.Build() require.NoError(t, err) @@ -688,10 +688,10 @@ func TestBuilder_Build_SAMLAutoGeneration(t *testing.T) { func TestBuilder_Build_SAMLFromEnv(t *testing.T) { env := requiredEnv() - env["SAML_CERTIFICATE"] = "env-cert" - env["SAML_PRIVATE_KEY"] = "env-key" + env["PROBOD_SAML_CERTIFICATE"] = "env-cert" + env["PROBOD_SAML_PRIVATE_KEY"] = "env-key" - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) cfg, err := b.Build() require.NoError(t, err) @@ -701,7 +701,7 @@ func TestBuilder_Build_SAMLFromEnv(t *testing.T) { } func TestBuilder_Build_SAMLPreset(t *testing.T) { - b := NewBuilder(mockEnv(requiredEnv())) + b := NewBuilder(NewResolver(mockEnv(requiredEnv()))) b.samlCertificate = "preset-cert" b.samlPrivateKey = "preset-key" @@ -713,7 +713,7 @@ func TestBuilder_Build_SAMLPreset(t *testing.T) { } func TestBuilder_Build_OAuth2Defaults(t *testing.T) { - b := NewBuilder(mockEnv(requiredEnv())) + b := NewBuilder(NewResolver(mockEnv(requiredEnv()))) cfg, err := b.Build() require.NoError(t, err) @@ -733,15 +733,15 @@ func TestBuilder_Build_OAuth2Defaults(t *testing.T) { func TestBuilder_Build_OAuth2FromEnv(t *testing.T) { env := requiredEnv() - env["OAUTH2_SERVER_SIGNING_KEY"] = "env-signing-key" - env["OAUTH2_SERVER_SIGNING_KEY_KID"] = "env-kid" - env["OAUTH2_SERVER_ACCESS_TOKEN_DURATION"] = "10" - env["OAUTH2_SERVER_REFRESH_TOKEN_DURATION"] = "20" - env["OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION"] = "30" - env["OAUTH2_SERVER_DEVICE_CODE_DURATION"] = "40" - env["OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS"] = "https://chatgpt.com/oauth/client.json,https://claude.ai/oauth/client.json" + env["PROBOD_OAUTH2_SERVER_SIGNING_KEY"] = "env-signing-key" + env["PROBOD_OAUTH2_SERVER_SIGNING_KEY_KID"] = "env-kid" + env["PROBOD_OAUTH2_SERVER_ACCESS_TOKEN_DURATION"] = "10" + env["PROBOD_OAUTH2_SERVER_REFRESH_TOKEN_DURATION"] = "20" + env["PROBOD_OAUTH2_SERVER_AUTHORIZATION_CODE_DURATION"] = "30" + env["PROBOD_OAUTH2_SERVER_DEVICE_CODE_DURATION"] = "40" + env["PROBOD_OAUTH2_SERVER_CIMD_ALLOWED_CLIENT_IDS"] = "https://chatgpt.com/oauth/client.json,https://claude.ai/oauth/client.json" - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) cfg, err := b.Build() require.NoError(t, err) @@ -770,7 +770,7 @@ func TestBuilder_Build_OAuth2Preset(t *testing.T) { env := requiredEnv() delete(env, "OAUTH2_SERVER_SIGNING_KEY") - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) b.oauth2SigningKey = "preset-signing-key" cfg, err := b.Build() @@ -782,9 +782,9 @@ func TestBuilder_Build_OAuth2Preset(t *testing.T) { func TestBuilder_Build_PgCABundleFromEnv(t *testing.T) { env := requiredEnv() - env["PG_CA_BUNDLE"] = "test-ca-bundle-content" + env["PROBOD_PG_CA_BUNDLE"] = "test-ca-bundle-content" - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) b.samlCertificate = "test-cert" b.samlPrivateKey = "test-key" @@ -801,9 +801,9 @@ func TestBuilder_Build_PgCABundleFromFile(t *testing.T) { require.NoError(t, err) env := requiredEnv() - env["PG_CA_BUNDLE_PATH"] = caFile + env["PROBOD_PG_CA_BUNDLE_PATH"] = caFile - b := NewBuilder(mockEnv(env)) + b := NewBuilder(NewResolver(mockEnv(env))) b.samlCertificate = "test-cert" b.samlPrivateKey = "test-key" diff --git a/pkg/bootstrap/resolver.go b/pkg/bootstrap/resolver.go new file mode 100644 index 000000000..1a82e92a9 --- /dev/null +++ b/pkg/bootstrap/resolver.go @@ -0,0 +1,225 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package bootstrap + +import ( + "context" + "fmt" + "os" + "strconv" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" +) + +const awsSecretRefPrefix = "aws://" + +type ( + SecretsManagerClient interface { + GetSecretValue( + ctx context.Context, + params *secretsmanager.GetSecretValueInput, + optFns ...func(*secretsmanager.Options), + ) (*secretsmanager.GetSecretValueOutput, error) + } + + Resolver struct { + lookup EnvGetter + secretsManagerClient SecretsManagerClient + smCache map[string]string + err error + } +) + +func NewResolver(lookup EnvGetter) *Resolver { + if lookup == nil { + lookup = os.Getenv + } + + return &Resolver{lookup: lookup} +} + +func (r *Resolver) Err() error { + return r.err +} + +func (r *Resolver) getEnv(key string) string { + if r.err != nil { + return "" + } + + value, err := r.resolve(key) + if err != nil { + r.err = err + return "" + } + + return value +} + +func (r *Resolver) getEnvOrDefault(key, defaultValue string) string { + if value := r.getEnv(key); value != "" { + return value + } + + return defaultValue +} + +func (r *Resolver) getEnvIntOrDefault(key string, defaultValue int) int { + if value := r.getEnv(key); value != "" { + if intValue, err := strconv.ParseInt(value, 10, 32); err == nil { + return int(intValue) + } + } + + return defaultValue +} + +func (r *Resolver) getEnvFloatOrDefault(key string, defaultValue float64) float64 { + if value := r.getEnv(key); value != "" { + if floatValue, err := strconv.ParseFloat(value, 64); err == nil { + return floatValue + } + } + + return defaultValue +} + +func (r *Resolver) getEnvFloatPtr(key string) *float64 { + if value := r.getEnv(key); value != "" { + if floatValue, err := strconv.ParseFloat(value, 64); err == nil { + return &floatValue + } + } + + return nil +} + +func (r *Resolver) getEnvIntPtr(key string) *int { + if value := r.getEnv(key); value != "" { + if intValue, err := strconv.ParseInt(value, 10, 32); err == nil { + v := int(intValue) + return &v + } + } + + return nil +} + +func (r *Resolver) getEnvBoolOrDefault(key string, defaultValue bool) bool { + if value := r.getEnv(key); value != "" { + if boolValue, err := strconv.ParseBool(value); err == nil { + return boolValue + } + } + + return defaultValue +} + +func (r *Resolver) resolve(key string) (string, error) { + raw := r.lookup(key) + + secretID, ok := parseAWSSecretRef(raw) + if !ok { + return raw, nil + } + + value, err := r.loadPlaintextSecret(secretID) + if err != nil { + return "", fmt.Errorf("cannot resolve %s: %w", key, err) + } + + return value, nil +} + +func (r *Resolver) loadPlaintextSecret(secretID string) (string, error) { + if r.smCache != nil { + if value, ok := r.smCache[secretID]; ok { + return value, nil + } + } + + value, err := fetchPlaintextSecret( + context.Background(), + secretsManagerOptions{ + SecretID: secretID, + Client: r.secretsManagerClient, + }, + ) + if err != nil { + return "", err + } + + if r.smCache == nil { + r.smCache = make(map[string]string) + } + + r.smCache[secretID] = value + + return value, nil +} + +func parseAWSSecretRef(value string) (string, bool) { + if !strings.HasPrefix(value, awsSecretRefPrefix) { + return "", false + } + + secretID := strings.TrimPrefix(value, awsSecretRefPrefix) + if secretID == "" { + return "", false + } + + return secretID, true +} + +type secretsManagerOptions struct { + SecretID string + Client SecretsManagerClient +} + +func fetchPlaintextSecret(ctx context.Context, opts secretsManagerOptions) (string, error) { + client, err := secretsManagerClient(ctx, opts) + if err != nil { + return "", err + } + + out, err := client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{ + SecretId: aws.String(opts.SecretID), + }) + if err != nil { + return "", fmt.Errorf("cannot load secret from AWS Secrets Manager: %w", err) + } + + if out.SecretString == nil || *out.SecretString == "" { + return "", fmt.Errorf("secret %q has an empty SecretString", opts.SecretID) + } + + return *out.SecretString, nil +} + +func secretsManagerClient(ctx context.Context, opts secretsManagerOptions) (SecretsManagerClient, error) { + if opts.Client != nil { + return opts.Client, nil + } + + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + return nil, fmt.Errorf("cannot load AWS config: %w", err) + } + + return secretsmanager.NewFromConfig(cfg), nil +} diff --git a/pkg/bootstrap/resolver_test.go b/pkg/bootstrap/resolver_test.go new file mode 100644 index 000000000..0a28f92fd --- /dev/null +++ b/pkg/bootstrap/resolver_test.go @@ -0,0 +1,179 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package bootstrap + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/secretsmanager" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type mockSecretsManagerClient struct { + secrets map[string]string + called []string +} + +func (m *mockSecretsManagerClient) GetSecretValue( + _ context.Context, + params *secretsmanager.GetSecretValueInput, + _ ...func(*secretsmanager.Options), +) (*secretsmanager.GetSecretValueOutput, error) { + secretID := aws.ToString(params.SecretId) + m.called = append(m.called, secretID) + + value, ok := m.secrets[secretID] + if !ok { + return nil, fmt.Errorf("secret %q not found", secretID) + } + + return &secretsmanager.GetSecretValueOutput{ + SecretString: aws.String(value), + }, nil +} + +func testResolver(env map[string]string, client SecretsManagerClient) *Resolver { + r := NewResolver(mockEnv(env)) + r.secretsManagerClient = client + + return r +} + +func TestResolver_ResolveLiteralEnv(t *testing.T) { + t.Parallel() + + r := NewResolver(mockEnv(map[string]string{"PROBOD_FOO": "bar"})) + assert.Equal(t, "bar", r.getEnv("PROBOD_FOO")) + require.NoError(t, r.Err()) +} + +func TestBuilder_Build_ResolvesAWSSecretRefs(t *testing.T) { + t.Parallel() + + client := &mockSecretsManagerClient{ + secrets: map[string]string{ + "probo/sandbox/probod/encryption_key": "test-encryption-key-32-bytes-long", + "probo/sandbox/probod/cookie_secret": "test-cookie-secret-32-bytes-long!", + "probo/sandbox/probod/password_pepper": "test-password-pepper-32-bytes-lo", + "probo/sandbox/probod/oauth2_signing_key": "test-oauth2-signing-key", + }, + } + + env := map[string]string{ + "PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key", + "PROBOD_AUTH_COOKIE_SECRET": "aws://probo/sandbox/probod/cookie_secret", + "PROBOD_AUTH_PASSWORD_PEPPER": "aws://probo/sandbox/probod/password_pepper", + "PROBOD_OAUTH2_SERVER_SIGNING_KEY": "aws://probo/sandbox/probod/oauth2_signing_key", + "PROBOD_BASE_URL": "https://app.example.com", + } + b := NewBuilder(testResolver(env, client)) + + cfg, err := b.Build() + require.NoError(t, err) + require.Len(t, client.called, 4) + + assert.Equal(t, "test-encryption-key-32-bytes-long", cfg.Probod.EncryptionKey) + assert.Equal(t, "https://app.example.com", cfg.Probod.BaseURL) +} + +func TestResolver_ResolveAWSSecretRefsCachesBySecretID(t *testing.T) { + t.Parallel() + + client := &mockSecretsManagerClient{ + secrets: map[string]string{ + "probo/sandbox/probod/encryption_key": "secret-value", + }, + } + r := testResolver(map[string]string{ + "PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key", + "PROBOD_AUTH_COOKIE_SECRET": "aws://probo/sandbox/probod/encryption_key", + "PROBOD_AUTH_PASSWORD_PEPPER": "plaintext-pepper", + "PROBOD_OAUTH2_SERVER_SIGNING_KEY": "plaintext-oauth2", + }, client) + + assert.Equal(t, "secret-value", r.getEnv("PROBOD_ENCRYPTION_KEY")) + assert.Equal(t, "secret-value", r.getEnv("PROBOD_AUTH_COOKIE_SECRET")) + require.NoError(t, r.Err()) + require.Len(t, client.called, 1) +} + +func TestBuilder_Build_MixedPlaintextAndAWSSecrets(t *testing.T) { + t.Parallel() + + client := &mockSecretsManagerClient{ + secrets: map[string]string{ + "probo/sandbox/probod/encryption_key": "test-encryption-key-32-bytes-long", + }, + } + b := NewBuilder(testResolver(map[string]string{ + "PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key", + "PROBOD_AUTH_COOKIE_SECRET": "test-cookie-secret-32-bytes-long!", + "PROBOD_AUTH_PASSWORD_PEPPER": "test-password-pepper-32-bytes-lo", + "PROBOD_OAUTH2_SERVER_SIGNING_KEY": "test-oauth2-signing-key", + }, client)) + + cfg, err := b.Build() + require.NoError(t, err) + assert.Equal(t, "test-encryption-key-32-bytes-long", cfg.Probod.EncryptionKey) + assert.Equal(t, "test-cookie-secret-32-bytes-long!", cfg.Probod.Auth.Cookie.Secret) +} + +func TestResolver_ResolveAWSSecretRefMissingSecret(t *testing.T) { + t.Parallel() + + client := &mockSecretsManagerClient{secrets: map[string]string{}} + r := testResolver(map[string]string{ + "PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key", + }, client) + + assert.Empty(t, r.getEnv("PROBOD_ENCRYPTION_KEY")) + require.Error(t, r.Err()) + assert.Contains(t, r.Err().Error(), "cannot resolve PROBOD_ENCRYPTION_KEY") +} + +func TestResolver_ResolveAWSSecretRefEmptySecretString(t *testing.T) { + t.Parallel() + + client := &mockSecretsManagerClient{ + secrets: map[string]string{ + "probo/sandbox/probod/encryption_key": "", + }, + } + r := testResolver(map[string]string{ + "PROBOD_ENCRYPTION_KEY": "aws://probo/sandbox/probod/encryption_key", + }, client) + + assert.Empty(t, r.getEnv("PROBOD_ENCRYPTION_KEY")) + require.Error(t, r.Err()) + assert.Contains(t, r.Err().Error(), "empty SecretString") +} + +func TestParseAWSSecretRef(t *testing.T) { + t.Parallel() + + secretID, ok := parseAWSSecretRef("aws://probo/sandbox/probod/encryption_key") + require.True(t, ok) + assert.Equal(t, "probo/sandbox/probod/encryption_key", secretID) + + _, ok = parseAWSSecretRef("literal-value") + assert.False(t, ok) + + _, ok = parseAWSSecretRef("aws://") + assert.False(t, ok) +}