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.
|
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:
|
For a local/manual Cloud Build proof:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -97,6 +108,7 @@ The check is read-only and prints no secret values. It verifies:
|
||||||
- Source SQLite/KB backup/export repeatability.
|
- Source SQLite/KB backup/export repeatability.
|
||||||
- Whether an approved source KB/Postgres dump or replication credential exists.
|
- 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 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
|
## 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:
|
def _rule_allows_port(rule: dict, port: str) -> bool:
|
||||||
for allowed in rule.get("allowed", []):
|
for allowed in rule.get("allowed", []):
|
||||||
if allowed.get("IPProtocol") in ("tcp", "all"):
|
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_contract", check_cloud_build_contract),
|
||||||
safe_check("cloud_build_service_account", check_cloud_build_service_account),
|
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_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("network_ingress", check_network_ingress),
|
||||||
safe_check("compute_runtime_service_accounts", check_compute_runtime_service_accounts),
|
safe_check("compute_runtime_service_accounts", check_compute_runtime_service_accounts),
|
||||||
safe_check("compute_disk_snapshots", check_snapshot_policy),
|
safe_check("compute_disk_snapshots", check_snapshot_policy),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue