208 lines
8.2 KiB
YAML
208 lines
8.2 KiB
YAML
name: gcp-observatory-read-adapter
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
action:
|
|
description: Build only, or deploy the protected staging service and run live receipts
|
|
required: true
|
|
default: build_only
|
|
type: choice
|
|
options:
|
|
- build_only
|
|
- deploy_staging
|
|
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
|
|
concurrency:
|
|
group: gcp-observatory-read-adapter-${{ github.ref }}
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
PROJECT_ID: teleo-501523
|
|
REGION: europe-west6
|
|
ZONE: europe-west6-a
|
|
REPOSITORY: teleo
|
|
IMAGE_NAME: observatory-read-adapter
|
|
SERVICE_NAME: observatory-read-adapter-staging
|
|
RUNTIME_SERVICE_ACCOUNT: sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com
|
|
DB_IAM_USER: sa-observatory-read-adapter@teleo-501523.iam
|
|
API_KEY_SECRET: observatory-read-api-key-staging
|
|
WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github
|
|
DEPLOY_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com
|
|
|
|
jobs:
|
|
build:
|
|
name: Test, build, and push adapter image
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
outputs:
|
|
image_ref: ${{ steps.image.outputs.image_ref }}
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.11"
|
|
cache: pip
|
|
|
|
- name: Run focused adapter tests
|
|
run: |
|
|
python -m pip install -e '.[dev]'
|
|
python -m pytest tests/test_observatory_read_adapter.py -q
|
|
python -m ruff check observatory_read_adapter tests/test_observatory_read_adapter.py
|
|
|
|
- id: auth
|
|
uses: google-github-actions/auth@v3
|
|
with:
|
|
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
|
|
service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }}
|
|
|
|
- uses: google-github-actions/setup-gcloud@v3
|
|
with:
|
|
project_id: ${{ env.PROJECT_ID }}
|
|
|
|
- name: Configure Artifact Registry
|
|
run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet
|
|
|
|
- id: image
|
|
name: Build, smoke, and push immutable image
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
tag="${GITHUB_SHA::12}"
|
|
image="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:${tag}"
|
|
digest="$(gcloud artifacts docker images describe "${image}" \
|
|
--project="${PROJECT_ID}" \
|
|
--format='value(image_summary.digest)' 2>/dev/null || true)"
|
|
if [[ -n "${digest}" ]]; then
|
|
docker pull "${image}"
|
|
else
|
|
docker build -f Dockerfile.observatory-read-adapter -t "${image}" .
|
|
fi
|
|
docker run --rm --env PYTHONPYCACHEPREFIX=/tmp/pycache \
|
|
"${image}" python -m compileall -q /app/observatory_read_adapter
|
|
docker run --rm "${image}" python -c 'import observatory_read_adapter; print("adapter-import-ok")'
|
|
if [[ -z "${digest}" ]]; then
|
|
docker push "${image}" | tee docker-push.log
|
|
digest="$(awk '/digest: sha256:/ {print $3}' docker-push.log | tail -1)"
|
|
fi
|
|
test -n "${digest}"
|
|
image_ref="${image}@${digest}"
|
|
printf 'image_ref=%s\n' "${image_ref}" >> "${GITHUB_OUTPUT}"
|
|
printf 'revision=%s\nimage_ref=%s\n' "${GITHUB_SHA}" "${image_ref}" > observatory-adapter-image.txt
|
|
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: observatory-adapter-image
|
|
path: observatory-adapter-image.txt
|
|
if-no-files-found: error
|
|
|
|
deploy-staging:
|
|
name: Deploy and prove staging read path
|
|
if: ${{ inputs.action == 'deploy_staging' }}
|
|
needs: build
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- id: auth
|
|
uses: google-github-actions/auth@v3
|
|
with:
|
|
workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }}
|
|
service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }}
|
|
|
|
- uses: google-github-actions/setup-gcloud@v3
|
|
with:
|
|
project_id: ${{ env.PROJECT_ID }}
|
|
|
|
- name: Deploy bounded GCP staging service
|
|
shell: bash
|
|
env:
|
|
IMAGE_REF: ${{ needs.build.outputs.image_ref }}
|
|
run: |
|
|
set -euo pipefail
|
|
gcloud run deploy "${SERVICE_NAME}" \
|
|
--project="${PROJECT_ID}" \
|
|
--region="${REGION}" \
|
|
--image="${IMAGE_REF}" \
|
|
--execution-environment=gen2 \
|
|
--service-account="${RUNTIME_SERVICE_ACCOUNT}" \
|
|
--network=teleo-staging-net \
|
|
--subnet=teleo-staging-europe-west6 \
|
|
--vpc-egress=private-ranges-only \
|
|
--ingress=all \
|
|
--allow-unauthenticated \
|
|
--port=8080 \
|
|
--cpu=1 \
|
|
--memory=512Mi \
|
|
--concurrency=8 \
|
|
--max-instances=2 \
|
|
--timeout=15 \
|
|
--set-secrets="OBSERVATORY_API_KEY=${API_KEY_SECRET}:latest" \
|
|
--set-env-vars="GCP_PROJECT_ID=${PROJECT_ID},CLOUD_SQL_INSTANCE=${PROJECT_ID}:${REGION}:teleo-pgvector-standby,DB_NAME=teleo_canonical,DB_IAM_USER=${DB_IAM_USER},DB_AUTHORIZATION_ROLE=kb_observatory_read,SERVICE_REVISION=${GITHUB_SHA}" \
|
|
--labels="livingip-tier=gcp-staging,livingip-surface=observatory-read-adapter" \
|
|
--quiet
|
|
|
|
- name: Capture positive and negative live receipts
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
service_url="$(gcloud run services describe "${SERVICE_NAME}" --project="${PROJECT_ID}" --region="${REGION}" --format='value(status.url)')"
|
|
revision="$(gcloud run services describe "${SERVICE_NAME}" --project="${PROJECT_ID}" --region="${REGION}" --format='value(status.latestReadyRevisionName)')"
|
|
api_key="$(gcloud secrets versions access latest --secret="${API_KEY_SECRET}" --project="${PROJECT_ID}")"
|
|
test -n "${service_url}"
|
|
test -n "${revision}"
|
|
test -n "${api_key}"
|
|
|
|
curl --fail --silent --show-error \
|
|
--header "X-Api-Key: ${api_key}" \
|
|
"${service_url}/v1/claims/sample" > positive.json
|
|
jq -e '
|
|
.schema == "livingip.observatory-canonical-claim.v1"
|
|
and .read_only == true
|
|
and .provenance.database == "teleo_canonical"
|
|
and .provenance.authorization_role == "kb_observatory_read"
|
|
and .provenance.transaction_read_only == true
|
|
and .provenance.write_privileges_denied == true
|
|
and (.canonical.evidence | length) > 0
|
|
and .proposal_ledger.distinct_from_canonical == true
|
|
' positive.json >/dev/null
|
|
|
|
anonymous_status="$(curl --silent --output anonymous.json --write-out '%{http_code}' "${service_url}/v1/claims/sample")"
|
|
write_status="$(curl --silent --output write.json --write-out '%{http_code}' \
|
|
--request POST --header "X-Api-Key: ${api_key}" --header 'Content-Type: application/json' \
|
|
--data '{"status":"applied"}' "${service_url}/v1/claims/sample")"
|
|
test "${anonymous_status}" = 401
|
|
test "${write_status}" = 405
|
|
|
|
jq -n \
|
|
--arg service_url "${service_url}" \
|
|
--arg revision "${revision}" \
|
|
--arg git_sha "${GITHUB_SHA}" \
|
|
--argjson positive "$(cat positive.json)" \
|
|
--arg anonymous_status "${anonymous_status}" \
|
|
--arg write_status "${write_status}" \
|
|
'{
|
|
schema: "livingip.observatory-read-adapter-live-receipt.v1",
|
|
required_tier: "T3_live_readonly",
|
|
service_url: $service_url,
|
|
revision: $revision,
|
|
git_sha: $git_sha,
|
|
positive: $positive,
|
|
negative: {
|
|
anonymous_http_status: ($anonymous_status | tonumber),
|
|
authenticated_post_http_status: ($write_status | tonumber)
|
|
},
|
|
production_repoint_executed: false
|
|
}' > observatory-read-adapter-live-receipt.json
|
|
unset api_key
|
|
|
|
- uses: actions/upload-artifact@v4
|
|
with:
|
|
name: observatory-read-adapter-live-receipt
|
|
path: observatory-read-adapter-live-receipt.json
|
|
if-no-files-found: error
|