From 6ea5920c85f527ddab8561a3d3b0cb87cb69cd11 Mon Sep 17 00:00:00 2001 From: Lectom Date: Mon, 22 Jun 2026 13:32:00 +0900 Subject: [PATCH] Harden WORKS image deploy startup path --- .gitea/workflows/production_image_deploy.yml | 1 + .gitea/workflows/staging_image_deploy.yml | 1 + scripts/deploy/upload_and_run_image_deploy.sh | 71 ++++++++++++------- .../production_image_workflows_policy_test.sh | 18 ++++- 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/.gitea/workflows/production_image_deploy.yml b/.gitea/workflows/production_image_deploy.yml index 62ffe931..7bf0c189 100644 --- a/.gitea/workflows/production_image_deploy.yml +++ b/.gitea/workflows/production_image_deploy.yml @@ -110,6 +110,7 @@ jobs: WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID }} WORKS_DRIVE_DOCKER_IMAGE_DIR: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DIR || 'baron-sso' }} WORKS_ADMIN_API_BASE_URL: ${{ vars.WORKS_ADMIN_API_BASE_URL }} + WORKS_ADMIN_OAUTH_TOKEN_URL: ${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }} WORKS_DRIVE_ACCESS_TOKEN_INPUT: ${{ secrets.WORKS_DRIVE_ACCESS_TOKEN }} WORKS_DRIVE_ACCESS_TOKEN_FILE: ${{ vars.WORKS_DRIVE_ACCESS_TOKEN_FILE }} WORKS_DRIVE_ACCESS_TOKEN_CMD: ${{ vars.WORKS_DRIVE_ACCESS_TOKEN_CMD }} diff --git a/.gitea/workflows/staging_image_deploy.yml b/.gitea/workflows/staging_image_deploy.yml index 84f85eca..12656dec 100644 --- a/.gitea/workflows/staging_image_deploy.yml +++ b/.gitea/workflows/staging_image_deploy.yml @@ -108,6 +108,7 @@ jobs: WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID }} WORKS_DRIVE_DOCKER_IMAGE_DIR: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DIR || 'baron-sso' }} WORKS_ADMIN_API_BASE_URL: ${{ vars.WORKS_ADMIN_API_BASE_URL }} + WORKS_ADMIN_OAUTH_TOKEN_URL: ${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }} WORKS_DRIVE_ACCESS_TOKEN_INPUT: ${{ secrets.WORKS_DRIVE_ACCESS_TOKEN }} WORKS_DRIVE_ACCESS_TOKEN_FILE: ${{ vars.WORKS_DRIVE_ACCESS_TOKEN_FILE }} WORKS_DRIVE_ACCESS_TOKEN_CMD: ${{ vars.WORKS_DRIVE_ACCESS_TOKEN_CMD }} diff --git a/scripts/deploy/upload_and_run_image_deploy.sh b/scripts/deploy/upload_and_run_image_deploy.sh index 3dde6cc5..c59548c6 100755 --- a/scripts/deploy/upload_and_run_image_deploy.sh +++ b/scripts/deploy/upload_and_run_image_deploy.sh @@ -19,7 +19,41 @@ require_env WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID [[ -f "$IMAGE_DEPLOY_BUNDLE_FILE" ]] || die "bundle file not found: $IMAGE_DEPLOY_BUNDLE_FILE" +log() { + printf '==> %s\n' "$*" >&2 +} + +refresh_works_drive_access_token() { + [[ -n "${WORKS_DRIVE_OAUTH_CLIENT_ID:-}" ]] || die "WORKS_DRIVE_OAUTH_CLIENT_ID is required when using WORKS_DRIVE_OAUTH_REFRESH_TOKEN." + [[ -n "${WORKS_DRIVE_OAUTH_CLIENT_SECRET:-}" ]] || die "WORKS_DRIVE_OAUTH_CLIENT_SECRET is required when using WORKS_DRIVE_OAUTH_REFRESH_TOKEN." + [[ -n "${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}" ]] || die "WORKS_DRIVE_OAUTH_REFRESH_TOKEN is required for refresh-token mode." + + local token_url="${WORKS_ADMIN_OAUTH_TOKEN_URL:-${WORKS_DRIVE_OAUTH_TOKEN_URL:-https://auth.worksmobile.com/oauth2/v2.0/token}}" + local response + local access_token + local rotated_refresh_token + + log "Refreshing WORKS Drive access token" + response="$(curl -fsS -X POST "$token_url" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=refresh_token" \ + --data-urlencode "refresh_token=${WORKS_DRIVE_OAUTH_REFRESH_TOKEN}" \ + --data-urlencode "client_id=${WORKS_DRIVE_OAUTH_CLIENT_ID}" \ + --data-urlencode "client_secret=${WORKS_DRIVE_OAUTH_CLIENT_SECRET}")" + access_token="$(jq -er '.access_token' <<<"$response")" + rotated_refresh_token="$(jq -r '.refresh_token // empty' <<<"$response")" + if [[ -n "$rotated_refresh_token" && "$rotated_refresh_token" != "$WORKS_DRIVE_OAUTH_REFRESH_TOKEN" ]]; then + printf 'WARNING: WORKS returned a rotated refresh token. Update WORKS_DRIVE_REFRESH_TOKEN before the old token ages out.\n' >&2 + fi + printf '%s\n' "$access_token" +} + resolve_works_drive_access_token() { + if [[ -n "${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}" ]]; then + refresh_works_drive_access_token + return + fi + if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN:-}" ]]; then printf '%s\n' "$WORKS_DRIVE_ACCESS_TOKEN" return @@ -41,30 +75,6 @@ resolve_works_drive_access_token() { return fi - if [[ -n "${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}" ]]; then - [[ -n "${WORKS_DRIVE_OAUTH_CLIENT_ID:-}" ]] || die "WORKS_DRIVE_OAUTH_CLIENT_ID is required when using WORKS_DRIVE_OAUTH_REFRESH_TOKEN." - [[ -n "${WORKS_DRIVE_OAUTH_CLIENT_SECRET:-}" ]] || die "WORKS_DRIVE_OAUTH_CLIENT_SECRET is required when using WORKS_DRIVE_OAUTH_REFRESH_TOKEN." - - local token_url="${WORKS_DRIVE_OAUTH_TOKEN_URL:-https://auth.worksmobile.com/oauth2/v2.0/token}" - local response - local access_token - local rotated_refresh_token - - response="$(curl -fsS -X POST "$token_url" \ - -H "Content-Type: application/x-www-form-urlencoded" \ - --data-urlencode "grant_type=refresh_token" \ - --data-urlencode "refresh_token=${WORKS_DRIVE_OAUTH_REFRESH_TOKEN}" \ - --data-urlencode "client_id=${WORKS_DRIVE_OAUTH_CLIENT_ID}" \ - --data-urlencode "client_secret=${WORKS_DRIVE_OAUTH_CLIENT_SECRET}")" - access_token="$(jq -er '.access_token' <<<"$response")" - rotated_refresh_token="$(jq -r '.refresh_token // empty' <<<"$response")" - if [[ -n "$rotated_refresh_token" && "$rotated_refresh_token" != "$WORKS_DRIVE_OAUTH_REFRESH_TOKEN" ]]; then - printf 'WARNING: WORKS returned a rotated refresh token. Update WORKS_DRIVE_REFRESH_TOKEN before the old token ages out.\n' >&2 - fi - printf '%s\n' "$access_token" - return - fi - die "Missing WORKS Drive access auth. Provide WORKS_DRIVE_ACCESS_TOKEN, WORKS_DRIVE_ACCESS_TOKEN_FILE, WORKS_DRIVE_ACCESS_TOKEN_CMD, or WORKS_DRIVE_OAUTH_REFRESH_TOKEN." } @@ -87,6 +97,19 @@ printf '%s\n' "$works_drive_access_token" | ssh "${DEPLOY_USER}@${DEPLOY_HOST}" export WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID='${WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID:-}'; \ export WORKS_DRIVE_DOCKER_IMAGE_DIR='${WORKS_DRIVE_DOCKER_IMAGE_DIR:-baron-sso}'; \ export WORKS_ADMIN_API_BASE_URL='${WORKS_ADMIN_API_BASE_URL:-https://www.worksapis.com}'; \ + echo '==> Validating image deploy compose config'; \ + docker compose --env-file .env -f docker-compose.yml config >/dev/null; \ + echo '==> Downloading and loading WORKS Drive application images'; \ scripts/docker-image/download_works_drive.sh; \ + set -a; \ + . ./.env; \ + set +a; \ + echo '==> Verifying loaded application images'; \ + for image_ref in \"\${BACKEND_IMAGE_NAME}:\${IMAGE_TAG}\" \"\${USERFRONT_IMAGE_NAME}:\${IMAGE_TAG}\" \"\${ADMINFRONT_IMAGE_NAME}:\${IMAGE_TAG}\" \"\${DEVFRONT_IMAGE_NAME}:\${IMAGE_TAG}\" \"\${ORGFRONT_IMAGE_NAME}:\${IMAGE_TAG}\"; do \ + docker image inspect \"\${image_ref}\" >/dev/null; \ + done; \ + echo '==> Prefetching runtime images before compose up'; \ + docker compose --env-file .env -f docker-compose.yml pull --ignore-pull-failures; \ + echo '==> Starting production image stack'; \ docker compose --env-file .env -f docker-compose.yml up -d --remove-orphans; \ docker compose --env-file .env -f docker-compose.yml ps" diff --git a/test/production_image_workflows_policy_test.sh b/test/production_image_workflows_policy_test.sh index cefb7158..3637b2cc 100644 --- a/test/production_image_workflows_policy_test.sh +++ b/test/production_image_workflows_policy_test.sh @@ -104,6 +104,8 @@ grep -Fq "scripts/deploy/upload_and_run_image_deploy.sh" "$staging_deploy_workfl || fail "staging deploy workflow must use the shared remote deploy script." grep -Fq "WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID: \${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID }}" "$staging_deploy_workflow" \ || fail "staging deploy workflow must pass the Docker-image-specific WORKS Drive ID variable." +grep -Fq "WORKS_ADMIN_OAUTH_TOKEN_URL: \${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }}" "$staging_deploy_workflow" \ + || fail "staging deploy workflow must pass the WORKS OAuth token URL into the remote image deploy step." grep -Fq "name: Deploy Baron SSO Production Images" "$deploy_workflow" \ || fail "deploy workflow must have the expected name." @@ -125,20 +127,32 @@ grep -Fq "scripts/deploy/upload_and_run_image_deploy.sh" "$deploy_workflow" \ || fail "production deploy workflow must use the shared remote deploy script." grep -Fq "WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID: \${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID }}" "$deploy_workflow" \ || fail "production deploy workflow must pass the Docker-image-specific WORKS Drive ID variable." +grep -Fq "WORKS_ADMIN_OAUTH_TOKEN_URL: \${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }}" "$deploy_workflow" \ + || fail "production deploy workflow must pass the WORKS OAuth token URL into the remote image deploy step." grep -Fq "Same image tag contract as staging" "$deploy_workflow" \ || fail "production deploy workflow must document that it uses the same image tag as staging." grep -Fq "TRAEFIK_PUBLIC_NETWORK=traefik-public" "$bundle_script" \ || fail "shared bundle script must write Traefik public network env." grep -Fq "scripts/docker-image/download_works_drive.sh" "$remote_deploy_script" \ || fail "shared remote deploy script must load requested image archives from WORKS Drive before running." +grep -Fq "refresh_works_drive_access_token" "$remote_deploy_script" \ + || fail "shared remote deploy script must refresh WORKS Drive access tokens when a refresh token is available." +grep -Fq 'WORKS_ADMIN_OAUTH_TOKEN_URL:-${WORKS_DRIVE_OAUTH_TOKEN_URL:-https://auth.worksmobile.com/oauth2/v2.0/token}' "$remote_deploy_script" \ + || fail "shared remote deploy script must honor WORKS_ADMIN_OAUTH_TOKEN_URL for refresh-token grants." +grep -Fq "docker compose --env-file .env -f docker-compose.yml config" "$remote_deploy_script" \ + || fail "shared remote deploy script must validate the remote compose config before running." +grep -Fq "docker compose --env-file .env -f docker-compose.yml pull --ignore-pull-failures" "$remote_deploy_script" \ + || fail "shared remote deploy script must prefetch runtime images before compose up." +grep -Fq 'docker image inspect \"\${image_ref}\"' "$remote_deploy_script" \ + || fail "shared remote deploy script must inspect loaded application images before compose up." grep -Fq "docker load" "$works_image_download_script" \ || fail "WORKS Drive image download script must load downloaded archives into Docker." grep -Fq 'baron-sso/${IMAGE_TAG}/${image}.${IMAGE_TAG}.tar.zst' "$works_image_download_script" \ || fail "WORKS Drive image download script must document the normalized archive path." grep -Fq "docker compose --env-file .env -f docker-compose.yml up -d" "$remote_deploy_script" \ || fail "shared remote deploy script must start the stack after pulling images." -if grep -Eiq 'harbor|docker login|docker compose --env-file .env -f docker-compose.yml pull|HARBOR_' "$staging_deploy_workflow" "$deploy_workflow" "$remote_deploy_script"; then - fail "image deploy workflows/scripts must not depend on Harbor registry login or compose pull." +if grep -Eiq 'harbor|docker login|HARBOR_' "$staging_deploy_workflow" "$deploy_workflow" "$remote_deploy_script"; then + fail "image deploy workflows/scripts must not depend on Harbor registry login." fi if grep -Eq 'docker (build|commit)' "$staging_deploy_workflow" "$deploy_workflow"; then