Add GitHub WIF GCP readiness probe (#45)
Some checks are pending
CI / lint-and-test (push) Waiting to run
Some checks are pending
CI / lint-and-test (push) Waiting to run
This commit is contained in:
parent
6dd7f9fe8c
commit
0c9dde3f36
3 changed files with 149 additions and 0 deletions
98
.github/workflows/gcp-readiness.yml
vendored
Normal file
98
.github/workflows/gcp-readiness.yml
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
name: gcp-readiness
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: gcp-readiness-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
PROJECT_ID: teleo-501523
|
||||
WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github
|
||||
ARTIFACT_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com
|
||||
|
||||
jobs:
|
||||
readiness:
|
||||
name: Read-only GCP readiness probe
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- id: auth
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
|
||||
service_account: ${{ env.ARTIFACT_SERVICE_ACCOUNT }}
|
||||
|
||||
- uses: google-github-actions/setup-gcloud@v2
|
||||
|
||||
- name: Capture gcloud identity
|
||||
shell: bash
|
||||
run: |
|
||||
set +e
|
||||
mkdir -p .gcp-readiness
|
||||
{
|
||||
echo "project=${PROJECT_ID}"
|
||||
echo "service_account=${ARTIFACT_SERVICE_ACCOUNT}"
|
||||
echo "workflow_ref=${GITHUB_REF}"
|
||||
echo "revision=${GITHUB_SHA}"
|
||||
} > .gcp-readiness/context.txt
|
||||
gcloud auth list --format=json > .gcp-readiness/gcloud-auth-list.json 2> .gcp-readiness/gcloud-auth-list.stderr
|
||||
echo "$?" > .gcp-readiness/gcloud-auth-list.exitcode
|
||||
gcloud config list --format=json > .gcp-readiness/gcloud-config-list.json 2> .gcp-readiness/gcloud-config-list.stderr
|
||||
echo "$?" > .gcp-readiness/gcloud-config-list.exitcode
|
||||
|
||||
- name: Run readiness checker
|
||||
shell: bash
|
||||
run: |
|
||||
set +e
|
||||
python3 ops/check_gcp_infra_readiness.py \
|
||||
> .gcp-readiness/gcp-infra-readiness.json \
|
||||
2> .gcp-readiness/gcp-infra-readiness.stderr
|
||||
echo "$?" > .gcp-readiness/gcp-infra-readiness.exitcode
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
base = Path(".gcp-readiness")
|
||||
report = {
|
||||
"artifact": "github_wif_gcp_readiness_probe",
|
||||
"checker_exitcode": (base / "gcp-infra-readiness.exitcode").read_text().strip(),
|
||||
"stderr_tail": (base / "gcp-infra-readiness.stderr").read_text()[-2000:],
|
||||
}
|
||||
try:
|
||||
payload = json.loads((base / "gcp-infra-readiness.json").read_text() or "{}")
|
||||
except json.JSONDecodeError as exc:
|
||||
report["json_parse_error"] = str(exc)
|
||||
payload = {}
|
||||
if payload:
|
||||
report["pass_count"] = payload.get("pass_count")
|
||||
report["blocked_count"] = payload.get("blocked_count")
|
||||
report["fail_count"] = payload.get("fail_count")
|
||||
report["checks"] = [
|
||||
{
|
||||
"name": check.get("name"),
|
||||
"status": check.get("status"),
|
||||
"required_tier": check.get("required_tier"),
|
||||
"current_tier": check.get("current_tier"),
|
||||
"detail": (check.get("detail") or "")[:500],
|
||||
}
|
||||
for check in payload.get("checks", [])
|
||||
]
|
||||
(base / "gcp-readiness-summary.json").write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
|
||||
print(json.dumps(report, indent=2, sort_keys=True))
|
||||
PY
|
||||
|
||||
- name: Upload readiness artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gcp-readiness
|
||||
path: .gcp-readiness/
|
||||
if-no-files-found: error
|
||||
|
|
@ -54,6 +54,17 @@ gh workflow run gcp-artifact.yml --repo living-ip/teleo-infrastructure --ref mai
|
|||
|
||||
The workflow authenticates to GCP with Workload Identity Federation, builds `Dockerfile.gcp-staging`, runs the image smoke test, pushes the image, and uploads `gcp-artifact-image.txt` as a run artifact.
|
||||
|
||||
For a read-only GCP posture probe through the same Workload Identity path:
|
||||
|
||||
```bash
|
||||
gh workflow run gcp-readiness.yml --repo living-ip/teleo-infrastructure --ref main
|
||||
```
|
||||
|
||||
This workflow runs `ops/check_gcp_infra_readiness.py` from GitHub Actions and
|
||||
uploads stdout, stderr, exit code, and a summary as the `gcp-readiness` artifact.
|
||||
It is intentionally non-mutating. If the Artifact Registry service account lacks
|
||||
broader read permissions, the artifact is still the proof of the exact IAM gap.
|
||||
|
||||
For a local/manual Cloud Build proof:
|
||||
|
||||
```bash
|
||||
|
|
@ -97,6 +108,7 @@ The check is read-only and prints no secret values. It verifies:
|
|||
- Source SQLite/KB backup/export repeatability.
|
||||
- Whether an approved source KB/Postgres dump or replication credential exists.
|
||||
- Whether source data has actually been restored or replicated into GCP and queried.
|
||||
- Whether the GitHub WIF readiness workflow exists for non-local readback.
|
||||
|
||||
## Current Boundaries
|
||||
|
||||
|
|
|
|||
|
|
@ -253,6 +253,44 @@ def check_github_actions_artifact_ci() -> Check:
|
|||
)
|
||||
|
||||
|
||||
def check_github_actions_readiness_ci() -> Check:
|
||||
workflow = Path(".github/workflows/gcp-readiness.yml")
|
||||
if not workflow.exists():
|
||||
return Check(
|
||||
"github_actions_readiness_ci",
|
||||
"blocked",
|
||||
"missing .github/workflows/gcp-readiness.yml",
|
||||
required_tier="T3_live_readonly",
|
||||
current_tier="T0_spec",
|
||||
)
|
||||
|
||||
text = workflow.read_text()
|
||||
required = [
|
||||
"workflow_dispatch",
|
||||
"google-github-actions/auth@v2",
|
||||
"ops/check_gcp_infra_readiness.py",
|
||||
GITHUB_ARTIFACT_SERVICE_ACCOUNT,
|
||||
"gcp-readiness-summary.json",
|
||||
"actions/upload-artifact@v4",
|
||||
]
|
||||
absent = [item for item in required if item not in text]
|
||||
if absent:
|
||||
return Check(
|
||||
"github_actions_readiness_ci",
|
||||
"fail",
|
||||
f"workflow_missing={absent}",
|
||||
required_tier="T3_live_readonly",
|
||||
current_tier="T1_foundation",
|
||||
)
|
||||
return Check(
|
||||
"github_actions_readiness_ci",
|
||||
"pass",
|
||||
"workflow=.github/workflows/gcp-readiness.yml captures readiness stdout/stderr/exitcode through GitHub WIF",
|
||||
required_tier="T3_live_readonly",
|
||||
current_tier="T2_runtime",
|
||||
)
|
||||
|
||||
|
||||
def _rule_allows_port(rule: dict, port: str) -> bool:
|
||||
for allowed in rule.get("allowed", []):
|
||||
if allowed.get("IPProtocol") in ("tcp", "all"):
|
||||
|
|
@ -615,6 +653,7 @@ def main() -> int:
|
|||
safe_check("cloud_build_contract", check_cloud_build_contract),
|
||||
safe_check("cloud_build_service_account", check_cloud_build_service_account),
|
||||
safe_check("github_actions_artifact_ci", check_github_actions_artifact_ci),
|
||||
safe_check("github_actions_readiness_ci", check_github_actions_readiness_ci),
|
||||
safe_check("network_ingress", check_network_ingress),
|
||||
safe_check("compute_runtime_service_accounts", check_compute_runtime_service_accounts),
|
||||
safe_check("compute_disk_snapshots", check_snapshot_policy),
|
||||
|
|
|
|||
Loading…
Reference in a new issue