Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ public/static
/playwright-report/
/playwright/.cache/
/test-results/

# Python bytecode
__pycache__/
4 changes: 4 additions & 0 deletions bench/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ best-effort — a failure fails the bench job and alerts):

1. **Commit upsert:** the writer builds the commit row from `git show` and upserts it
(`ON CONFLICT (commit_sha) DO UPDATE`) before applying any record.
The row's `branch` column (migration 012) defaults to `develop`, the only branch the
ingest role may write from today, so the writer does not send it. The read service charts
only `develop` commits (`web/lib/branch.ts`); a writer that ingests another branch must set
`branch` on the commit row.
2. **Direct write:** `INSERT … ON CONFLICT (measurement_id) DO UPDATE` into RDS as the
least-privilege `bench_ingest` IAM role (IAM auth, `sslmode=verify-full`), against the
schema in this repo's `migrations/`. Each JSONL file applies in one transaction.
Expand Down
13 changes: 12 additions & 1 deletion bench/infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Single one-shot script. Provisions, in order:
7. The GitHub OIDC provider `token.actions.githubusercontent.com` (account-scoped — created once if not present).
8. An IAM role `GitHubBenchmarkSchemaRole` trusted to GitHub Actions OIDC for `vortex-data/vortex.dev` on `main` with `sts:AssumeRoleWithWebIdentity`. Permission policy: `rds-db:connect` scoped to the `migrator` Postgres user on the **instance** resource only (CI schema deploys connect to the public instance endpoint).
9. An IAM role `GitHubBenchmarkIngestRole` (PR-2.1) trusted to the same OIDC provider for `vortex-data/vortex` on `develop`. Permission policy: `rds-db:connect` scoped to the `bench_ingest` Postgres user on the instance resource only. This is the dedicated least-privilege identity for the Phase-2 CI dual-write ingest path, deliberately separate from the schema-deploy `migrator` identity so the high-frequency ingest path can write data but never run DDL or migrations.
10. An IAM role `GitHubBenchmarkReadRole` trusted to the same OIDC provider for `vortex-data/vortex` on any branch and on the repository's own pull requests. Permission policy: `rds-db:connect` scoped to the `bench_ci_read` Postgres user on the instance resource only. CI uses it to read the published numbers and diff them against a run's local numbers. `bench_ci_read` (migration `011`) can only `SELECT` the six data tables, and every one of its transactions is read-only.

The `migrator` Postgres user is created by `migrations/002_iam_db_user.sql` (PR-1.3) and the `bench_ingest` user by `migrations/004_ingest_role.sql` (PR-2.1); each OIDC role's permission ARN is pre-scoped to its user. `002` is applied as the RDS master because it creates a role (it needs `CREATEROLE` and grants the `rds_iam` / schema privileges the master holds), while `003` and `004` are applied as the master because they additionally grant on master-owned objects (the ledger and the six data tables, respectively). All three run during the one-time bootstrap; the migrator-run `schema-deploy` path then records them as already-applied.

Expand Down Expand Up @@ -84,6 +85,7 @@ The script prints the exact `gh variable set` commands; copy them from its outpu
| `RDS_BENCH_DB_NAME` | `vortex_bench` | All Postgres connections |
| `GH_BENCH_SCHEMA_ROLE_ARN` | the schema-deploy OIDC role ARN | `.github/workflows/bench-schema-deploy.yml` |
| `GH_BENCH_INGEST_ROLE_ARN` | the ingest OIDC role ARN | PR-2.4 dual-write ingest workflows (`bench.yml` / `sql-benchmarks.yml` / `v3-commit-metadata.yml`) |
| `GH_BENCH_READ_ROLE_ARN` | the read-only OIDC role ARN | `vortex-data/vortex` CI jobs that diff against the published numbers, as Postgres user `bench_ci_read` |

The Vercel reader also connects to the public instance endpoint. It is
configured through `BENCH_DB_*` Vercel env vars (see `bench/web/README.md`),
Expand Down Expand Up @@ -319,6 +321,8 @@ aws iam delete-role-policy --role-name GitHubBenchmarkSchemaRole --policy-name r
aws iam delete-role --role-name GitHubBenchmarkSchemaRole
aws iam delete-role-policy --role-name GitHubBenchmarkIngestRole --policy-name rds-db-connect-ingest
aws iam delete-role --role-name GitHubBenchmarkIngestRole
aws iam delete-role-policy --role-name GitHubBenchmarkReadRole --policy-name rds-db-connect-ci-read
aws iam delete-role --role-name GitHubBenchmarkReadRole
aws ec2 delete-security-group --group-name vortex-bench-sg
aws rds delete-db-subnet-group --db-subnet-group-name vortex-bench-subnet-group
# OIDC provider (account-scoped): only delete if no other workflow uses it
Expand Down Expand Up @@ -368,14 +372,21 @@ it had no client connections from 2026-05-31 to 2026-10-28.
`vortex-data/benchmarks-website` repository are excluded. Manual schema deploys must select `main`.
The old shared `GITHUB_REPO` input is rejected to prevent an ambiguous role assignment.

The read role is the exception. `READ_GITHUB_REPO` defaults to `vortex-data/vortex`, and the role
trusts `repo:<repo>:ref:refs/heads/*` and `repo:<repo>:pull_request`, so a benchmark run on a PR
or a feature branch can read the published numbers. That is safe because its database user cannot
write. GitHub does not issue an OIDC token to a `pull_request` run from a fork, so fork PRs cannot
assume it.

Reprovisioning replaces each role's trust policy with these inputs. It does not preserve manual
subjects. Review both generated policies offline before applying infrastructure changes:

```bash
bash infra/provision.sh --print-trust schema
bash infra/provision.sh --print-trust ingest
bash infra/provision.sh --print-trust read
python3 -m unittest discover -s infra/tests -v
```

The print commands require only `jq` and make no AWS calls. They do not apply the policy.
The schema and ingest permission policies remain scoped to their respective database users.
The schema, ingest and read permission policies remain scoped to their respective database users.
112 changes: 98 additions & 14 deletions bench/infra/provision.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# SPDX-FileCopyrightText: Copyright the Vortex contributors
#
# One-time bootstrap of the bench AWS infrastructure: RDS Postgres, its
# backups and alarms, and the GitHub Actions OIDC roles for schema deploys
# and ingest. Idempotent: safe to re-run; each step checks for an existing
# backups and alarms, and the GitHub Actions OIDC roles for schema deploys,
# ingest and CI reads. Idempotent: safe to re-run; each step checks for an existing
# resource and skips creation if found. See infra/README.md for the
# full operator runbook and infra/RESTORE.md for restores.
#
Expand Down Expand Up @@ -41,15 +41,18 @@
# (free storage, CPU, connections) and an RDS event subscription
# (failure, low storage). Subscribes `ALARM_HTTPS_ENDPOINT` (the
# incident.io CloudWatch alert source) when it is set.
# 9. Creates two GitHub Actions OIDC IAM roles trusted to
# `token.actions.githubusercontent.com`, each scoped to one branch of its
# repository and one instance dbuser:
# 9. Creates three GitHub Actions OIDC IAM roles trusted to
# `token.actions.githubusercontent.com`, each scoped to one instance dbuser:
# - `GitHubBenchmarkSchemaRole`: `rds-db:connect` on the `migrator`
# Postgres user (schema deploys; migration 002 in PR-1.3).
# Postgres user (schema deploys; migration 002 in PR-1.3). One branch.
# - `GitHubBenchmarkIngestRole`: `rds-db:connect` on the `bench_ingest`
# Postgres user (Phase-2 CI dual-write ingest; migration 004 in PR-2.1).
# One branch.
# - `GitHubBenchmarkReadRole`: `rds-db:connect` on the read-only
# `bench_ci_read` Postgres user (CI diffs against published numbers;
# migration 011). Any branch, and same-repository pull requests.
# 10. Prints a summary: the GitHub Actions vars to set (instance endpoint,
# region, DB name, both role ARNs).
# region, DB name, the role ARNs).
#
# Re-run safety: every aws-cli mutation goes through `ensure_*`
# functions that check for an existing resource first. A partially
Expand Down Expand Up @@ -93,10 +96,12 @@ readonly DB_LOG_RETENTION_DAYS="${DB_LOG_RETENTION_DAYS:-30}"
readonly ALARM_HTTPS_ENDPOINT="${ALARM_HTTPS_ENDPOINT:-}"
readonly SCHEMA_ROLE_NAME="${SCHEMA_ROLE_NAME:-GitHubBenchmarkSchemaRole}"
readonly INGEST_ROLE_NAME="${INGEST_ROLE_NAME:-GitHubBenchmarkIngestRole}"
readonly READ_ROLE_NAME="${READ_ROLE_NAME:-GitHubBenchmarkReadRole}"
readonly SCHEMA_GITHUB_REPO="${SCHEMA_GITHUB_REPO:-vortex-data/vortex.dev}"
readonly SCHEMA_GITHUB_BRANCH="${SCHEMA_GITHUB_BRANCH:-main}"
readonly INGEST_GITHUB_REPO="${INGEST_GITHUB_REPO:-vortex-data/vortex}"
readonly INGEST_GITHUB_BRANCH="${INGEST_GITHUB_BRANCH:-develop}"
readonly READ_GITHUB_REPO="${READ_GITHUB_REPO:-vortex-data/vortex}"
# Postgres role created by migrations/002 in PR-1.3; the OIDC role's
# rds-db:connect permission is scoped to this user. NOT overridable (unlike the
# other names above): the role name is hardcoded in migrations/002
Expand All @@ -111,6 +116,9 @@ readonly PG_MIGRATOR_ROLE="migrator"
# authenticate as `bench_ingest`, so an env override would scope the IAM grant to
# a user the migration never creates and silently break ingest auth.
readonly PG_INGEST_ROLE="bench_ingest"
# Postgres role created by migrations/011: read-only and IAM-only. NOT
# overridable, same rationale as PG_MIGRATOR_ROLE.
readonly PG_CI_READ_ROLE="bench_ci_read"

readonly TAG_KEY_PROJECT="Project"
readonly TAG_VAL_PROJECT="vortex-benchmarks"
Expand Down Expand Up @@ -532,7 +540,7 @@ ensure_alarms() {
}

# -----------------------------------------------------------------------------
# Step 7: GitHub Actions OIDC IAM roles for schema deploys and ingest.
# Step 7: GitHub Actions OIDC IAM roles for schema deploys, ingest and CI reads.
# -----------------------------------------------------------------------------

ensure_oidc_provider() {
Expand Down Expand Up @@ -571,6 +579,27 @@ github_trust_policy() {
}'
}

# The read role can only SELECT, so it trusts every branch and the repository's
# own pull requests. GitHub issues no OIDC token to a `pull_request` run from a
# fork, so the `pull_request` subject admits only branches in the repository.
github_read_trust_policy() {
jq -n --arg account "$TARGET_ACCOUNT" --arg repo "$1" '{
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Principal: {Federated: ("arn:aws:iam::" + $account + ":oidc-provider/token.actions.githubusercontent.com")},
Action: "sts:AssumeRoleWithWebIdentity",
Condition: {
StringEquals: {"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"},
StringLike: {"token.actions.githubusercontent.com:sub": [
("repo:" + $repo + ":ref:refs/heads/*"),
("repo:" + $repo + ":pull_request")
]}
}
}]
}'
}

ensure_schema_role() {
log "Step 7: GitHub Actions OIDC role ${SCHEMA_ROLE_NAME}."
ensure_oidc_provider
Expand Down Expand Up @@ -676,6 +705,57 @@ EOF
log " inline permissions policy applied (rds-db:connect for ${PG_INGEST_ROLE} on instance)."
}

ensure_read_role() {
log "Step 7c: GitHub Actions OIDC role ${READ_ROLE_NAME}."
ensure_oidc_provider

local trust_policy
trust_policy=$(github_read_trust_policy "$READ_GITHUB_REPO")

if READ_ROLE_ARN=$(aws iam get-role --role-name "$READ_ROLE_NAME" \
--query 'Role.Arn' --output text 2>/dev/null); then
log " exists: ${READ_ROLE_ARN}; updating trust policy."
aws iam update-assume-role-policy \
--role-name "$READ_ROLE_NAME" \
--policy-document "$trust_policy"
else
READ_ROLE_ARN=$(aws iam create-role \
--role-name "$READ_ROLE_NAME" \
--assume-role-policy-document "$trust_policy" \
--description "GitHub Actions OIDC role for read-only benchmarks-website Postgres access" \
--tags "Key=${TAG_KEY_PROJECT},Value=${TAG_VAL_PROJECT}" \
"Key=${TAG_KEY_OWNER},Value=${TAG_VAL_OWNER}" \
--query 'Role.Arn' --output text)
log " created: ${READ_ROLE_ARN}"
fi

# rds-db:connect for the bench_ci_read Postgres user on the INSTANCE resource
# only. That user's grants (migrations/011) are what make it read-only.
local rds_arn_account="$TARGET_ACCOUNT"

local permissions_policy
permissions_policy=$(cat <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["rds-db:connect"],
"Resource": [
"arn:aws:rds-db:${TARGET_REGION}:${rds_arn_account}:dbuser:${DB_RESOURCE_ID}/${PG_CI_READ_ROLE}"
]
}]
}
EOF
)

aws iam put-role-policy \
--role-name "$READ_ROLE_NAME" \
--policy-name "rds-db-connect-ci-read" \
--policy-document "$permissions_policy" \
>/dev/null
log " inline permissions policy applied (rds-db:connect for ${PG_CI_READ_ROLE} on instance)."
}

# -----------------------------------------------------------------------------
# Step 8: Summary.
# -----------------------------------------------------------------------------
Expand All @@ -694,6 +774,7 @@ Backup replica : ${BACKUP_REPLICA_REGION}, ${BACKUP_REPLICA_RETENTION_DAYS} da
Alarm topic : ${ALARM_TOPIC_ARN}
Schema role ARN : ${SCHEMA_ROLE_ARN}
Ingest role ARN : ${INGEST_ROLE_ARN}
Read role ARN : ${READ_ROLE_ARN}

NEXT STEPS:

Expand All @@ -706,6 +787,7 @@ NEXT STEPS:
gh variable set RDS_BENCH_DB_NAME --body "${DB_NAME}"
gh variable set GH_BENCH_SCHEMA_ROLE_ARN --body "${SCHEMA_ROLE_ARN}"
gh variable set GH_BENCH_INGEST_ROLE_ARN --body "${INGEST_ROLE_ARN}"
gh variable set GH_BENCH_READ_ROLE_ARN --body "${READ_ROLE_ARN}"

2. If ALARM_HTTPS_ENDPOINT was unset, subscribe the incident.io CloudWatch
alert source to the alarm topic:
Expand All @@ -717,11 +799,11 @@ NEXT STEPS:
--query 'DBInstances[0].[DBInstanceStatus,IAMDatabaseAuthenticationEnabled,DeletionProtection,MaxAllocatedStorage]'
# expected: available True True ${DB_MAX_ALLOCATED_STORAGE_GB}

4. Migrations create the Postgres '${PG_MIGRATOR_ROLE}' (002) and
'${PG_INGEST_ROLE}' (004) users; both OIDC roles' permission policies are
4. Migrations create the Postgres '${PG_MIGRATOR_ROLE}' (002),
'${PG_INGEST_ROLE}' (004) and '${PG_CI_READ_ROLE}' (011) users; the OIDC roles' permission policies are
already scoped to those users. During the one-time bootstrap, apply EVERY
migration carrying the '-- migrate-schema: requires-superuser' marker as the
RDS master (currently 002/004/005, which create roles + grants, and 006/007,
RDS master (currently 002/004/005/011, which create roles + grants, and 006/007,
which run DDL on the master-owned query_measurements table). The marker in
each file is authoritative; see bench/migrations/README.md
'Bootstrap ordering' for the contract. After the bootstrap, unmarked schema
Expand All @@ -736,17 +818,18 @@ EOF
# -----------------------------------------------------------------------------

main() {
[ -z "${GITHUB_REPO+x}" ] || die "GITHUB_REPO was replaced by SCHEMA_GITHUB_REPO and INGEST_GITHUB_REPO."
[ -z "${GITHUB_REPO+x}" ] || die "GITHUB_REPO was replaced by SCHEMA_GITHUB_REPO, INGEST_GITHUB_REPO and READ_GITHUB_REPO."
if [ "$#" -gt 0 ]; then
if [ "$#" -eq 2 ] && [ "$1" = "--print-trust" ]; then
case "$2" in
schema) github_trust_policy "$SCHEMA_GITHUB_REPO" "$SCHEMA_GITHUB_BRANCH" ;;
ingest) github_trust_policy "$INGEST_GITHUB_REPO" "$INGEST_GITHUB_BRANCH" ;;
*) die "Expected schema or ingest." ;;
read) github_read_trust_policy "$READ_GITHUB_REPO" ;;
*) die "Expected schema, ingest or read." ;;
esac
return
fi
die "Usage: provision.sh [--print-trust schema|ingest]"
die "Usage: provision.sh [--print-trust schema|ingest|read]"
fi
verify_prereqs
discover_default_vpc
Expand All @@ -759,6 +842,7 @@ main() {
ensure_alarms
ensure_schema_role
ensure_ingest_role
ensure_read_role
print_summary
}

Expand Down
15 changes: 14 additions & 1 deletion bench/infra/tests/test_oidc_trust.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
class TrustTests(unittest.TestCase):
def render(self, role, **overrides):
env = {k: v for k, v in os.environ.items()
if k not in {"GITHUB_REPO", "SCHEMA_GITHUB_REPO", "INGEST_GITHUB_REPO",
if k not in {"GITHUB_REPO", "SCHEMA_GITHUB_REPO", "INGEST_GITHUB_REPO", "READ_GITHUB_REPO",
"SCHEMA_GITHUB_BRANCH", "INGEST_GITHUB_BRANCH", "TARGET_ACCOUNT"}}
env.update(overrides)
return subprocess.run(["bash", str(SCRIPT), "--print-trust", role],
Expand Down Expand Up @@ -43,6 +43,19 @@ def test_repository_inputs_are_independent(self):
self.assertEqual(condition["StringEquals"]["token.actions.githubusercontent.com:sub"],
f"repo:example/{repo}:ref:refs/heads/{branch}")

def test_read_role_admits_branches_and_repository_pull_requests(self):
result = self.render("read")
self.assertEqual(result.returncode, 0, result.stderr)
statement, = json.loads(result.stdout)["Statement"]
self.assertEqual(statement["Action"], "sts:AssumeRoleWithWebIdentity")
self.assertEqual(statement["Condition"], {
"StringEquals": {"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"},
"StringLike": {"token.actions.githubusercontent.com:sub": [
"repo:vortex-data/vortex:ref:refs/heads/*",
"repo:vortex-data/vortex:pull_request",
]},
})

def test_shared_legacy_input_is_rejected(self):
result = self.render("schema", GITHUB_REPO="example/old")
self.assertNotEqual(result.returncode, 0)
Expand Down
Loading
Loading