diff --git a/.gitea/workflows/image_publish.yml b/.gitea/workflows/image_publish.yml index e48b2596..772fc223 100644 --- a/.gitea/workflows/image_publish.yml +++ b/.gitea/workflows/image_publish.yml @@ -24,7 +24,7 @@ jobs: DEVFRONT_URL: ${{ vars.DEVFRONT_URL }} ORGFRONT_URL: ${{ vars.ORGFRONT_URL }} VITE_OIDC_AUTHORITY: ${{ vars.VITE_OIDC_AUTHORITY }} - WORKS_DRIVE_SHARED_DRIVE_ID: ${{ vars.WORKS_DRIVE_SHARED_DRIVE_ID }} + WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID }} 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 }} @@ -40,7 +40,7 @@ jobs: fi required_values=" - ADMINFRONT_URL DEVFRONT_URL ORGFRONT_URL VITE_OIDC_AUTHORITY WORKS_DRIVE_SHARED_DRIVE_ID + ADMINFRONT_URL DEVFRONT_URL ORGFRONT_URL VITE_OIDC_AUTHORITY WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID " for key in ${required_values}; do if [ -z "${!key:-}" ]; then @@ -213,18 +213,18 @@ jobs: - name: Upload built images to WORKS Drive archive env: IMAGE_TAG: ${{ steps.version.outputs.image_tag }} - WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR: ${{ vars.WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR }} + WORKS_DRIVE_DOCKER_IMAGE_DIR: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DIR }} WORKS_DRIVE_TARGET: sharedrive - WORKS_DRIVE_SHARED_DRIVE_ID: ${{ vars.WORKS_DRIVE_SHARED_DRIVE_ID }} - WORKS_DRIVE_PARENT_FILE_ID: ${{ vars.WORKS_DRIVE_PARENT_FILE_ID }} + WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID }} + WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID }} WORKS_ADMIN_API_BASE_URL: ${{ vars.WORKS_ADMIN_API_BASE_URL }} run: | set -euo pipefail - : "${WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR:=docker-build-image}" + : "${WORKS_DRIVE_DOCKER_IMAGE_DIR:=baron-sso}" required_values=" - IMAGE_TAG WORKS_DRIVE_SHARED_DRIVE_ID + IMAGE_TAG WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID " for key in ${required_values}; do if [ -z "${!key:-}" ]; then @@ -236,6 +236,9 @@ jobs: for image in backend userfront adminfront devfront orgfront; do image_ref="baron_sso/${image}:${IMAGE_TAG}" DOCKER_IMAGE_REF="${image_ref}" \ + WORKS_DRIVE_DOCKER_IMAGE_DIR="${WORKS_DRIVE_DOCKER_IMAGE_DIR}" \ + WORKS_DRIVE_SHARED_DRIVE_ID="${WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID}" \ + WORKS_DRIVE_PARENT_FILE_ID="${WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID:-}" \ WORKS_DOCKER_IMAGE_ARCHIVE_DIR="${RUNNER_TEMP}/baron-sso-docker-image-upload" \ scripts/docker-image/upload_works_drive.sh done diff --git a/.gitea/workflows/production_image_deploy.yml b/.gitea/workflows/production_image_deploy.yml index f1eb6e1f..62ffe931 100644 --- a/.gitea/workflows/production_image_deploy.yml +++ b/.gitea/workflows/production_image_deploy.yml @@ -81,12 +81,11 @@ jobs: OATHKEEPER_GID: ${{ vars.PROD_OATHKEEPER_GID }} OATHKEEPER_INTROSPECT_CLIENT_ID: ${{ vars.PROD_OATHKEEPER_INTROSPECT_CLIENT_ID }} ADMIN_EMAIL: ${{ vars.PROD_ADMIN_EMAIL }} - HARBOR_HOSTNAME: ${{ vars.HARBOR_HOSTNAME }} - BACKEND_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/backend - USERFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/userfront - ADMINFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/adminfront - DEVFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/devfront - ORGFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront + BACKEND_IMAGE_NAME: baron_sso/backend + USERFRONT_IMAGE_NAME: baron_sso/userfront + ADMINFRONT_IMAGE_NAME: baron_sso/adminfront + DEVFRONT_IMAGE_NAME: baron_sso/devfront + ORGFRONT_IMAGE_NAME: baron_sso/orgfront IMAGE_DEPLOY_DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }} IMAGE_DEPLOY_ORY_POSTGRES_PASSWORD: ${{ secrets.PROD_ORY_POSTGRES_PASSWORD }} IMAGE_DEPLOY_OATHKEEPER_INTROSPECT_CLIENT_SECRET: ${{ secrets.PROD_OATHKEEPER_INTROSPECT_CLIENT_SECRET }} @@ -107,9 +106,16 @@ jobs: DEPLOY_HOST: ${{ vars.PROD_HOST }} DEPLOY_USER: ${{ vars.PROD_USER }} DEPLOY_PATH: ${{ vars.PROD_DEPLOY_PATH }} - HARBOR_ENDPOINT: ${{ vars.HARBOR_ENDPOINT }} - HARBOR_ROBOT_ACCOUNT: ${{ vars.HARBOR_ROBOT_ACCOUNT }} - HARBOR_ROBOT_KEY: ${{ secrets.HARBOR_ROBOT_KEY }} + WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID }} + 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_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 }} + WORKS_DRIVE_OAUTH_CLIENT_ID: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_ID }} + WORKS_DRIVE_OAUTH_CLIENT_SECRET: ${{ secrets.WORKS_OAUTH_CLIENT_SECRET }} + WORKS_DRIVE_OAUTH_REFRESH_TOKEN: ${{ secrets.WORKS_DRIVE_REFRESH_TOKEN }} run: | set -euo pipefail scripts/deploy/upload_and_run_image_deploy.sh diff --git a/.gitea/workflows/staging_image_deploy.yml b/.gitea/workflows/staging_image_deploy.yml index 88fe17f3..84f85eca 100644 --- a/.gitea/workflows/staging_image_deploy.yml +++ b/.gitea/workflows/staging_image_deploy.yml @@ -81,12 +81,11 @@ jobs: OATHKEEPER_GID: ${{ vars.STG_OATHKEEPER_GID }} OATHKEEPER_INTROSPECT_CLIENT_ID: ${{ vars.STG_OATHKEEPER_INTROSPECT_CLIENT_ID }} ADMIN_EMAIL: ${{ vars.STG_ADMIN_EMAIL }} - HARBOR_HOSTNAME: ${{ vars.HARBOR_HOSTNAME }} - BACKEND_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/backend - USERFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/userfront - ADMINFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/adminfront - DEVFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/devfront - ORGFRONT_IMAGE_NAME: ${{ vars.HARBOR_HOSTNAME }}/baron_sso/orgfront + BACKEND_IMAGE_NAME: baron_sso/backend + USERFRONT_IMAGE_NAME: baron_sso/userfront + ADMINFRONT_IMAGE_NAME: baron_sso/adminfront + DEVFRONT_IMAGE_NAME: baron_sso/devfront + ORGFRONT_IMAGE_NAME: baron_sso/orgfront IMAGE_DEPLOY_DB_PASSWORD: ${{ secrets.STG_DB_PASSWORD }} IMAGE_DEPLOY_ORY_POSTGRES_PASSWORD: ${{ secrets.STG_ORY_POSTGRES_PASSWORD }} IMAGE_DEPLOY_OATHKEEPER_INTROSPECT_CLIENT_SECRET: ${{ secrets.STG_OATHKEEPER_INTROSPECT_CLIENT_SECRET }} @@ -105,9 +104,16 @@ jobs: DEPLOY_HOST: ${{ vars.STG_HOST }} DEPLOY_USER: ${{ vars.STG_USER }} DEPLOY_PATH: ${{ vars.STG_DEPLOY_PATH }} - HARBOR_ENDPOINT: ${{ vars.HARBOR_ENDPOINT }} - HARBOR_ROBOT_ACCOUNT: ${{ vars.HARBOR_ROBOT_ACCOUNT }} - HARBOR_ROBOT_KEY: ${{ secrets.HARBOR_ROBOT_KEY }} + WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID: ${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID }} + 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_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 }} + WORKS_DRIVE_OAUTH_CLIENT_ID: ${{ secrets.WORKS_DRIVE_OAUTH_CLIENT_ID }} + WORKS_DRIVE_OAUTH_CLIENT_SECRET: ${{ secrets.WORKS_OAUTH_CLIENT_SECRET }} + WORKS_DRIVE_OAUTH_REFRESH_TOKEN: ${{ secrets.WORKS_DRIVE_REFRESH_TOKEN }} run: | set -euo pipefail scripts/deploy/upload_and_run_image_deploy.sh diff --git a/docs/works-drive-docker-image-archive.md b/docs/works-drive-docker-image-archive.md index c4f431a0..df659636 100644 --- a/docs/works-drive-docker-image-archive.md +++ b/docs/works-drive-docker-image-archive.md @@ -18,9 +18,9 @@ Gitea Actions의 shared image publish workflow는 `baron_sso/:// - image.tar.zst - image.tar.zst.sha256 - manifest.json +baron-sso// + ..tar.zst + ..sha256 + manifest..json ``` 예시: ```text -docker-build-image/baron_sso/backend/v1.2606.ab12/ - image.tar.zst - image.tar.zst.sha256 - manifest.json +baron-sso/v1.2606.ab12/ + backend.v1.2606.ab12.tar.zst + backend.v1.2606.ab12.sha256 + manifest.v1.2606.ab12.json ``` -Registry hostname은 저장 경로에서 제외한다. 예를 들어 `registry.example/baron_sso/backend:v1.2606.ab12`는 `baron_sso/backend/v1.2606.ab12` 아래에 저장한다. +Registry hostname과 image namespace는 저장 경로에서 제외한다. 예를 들어 `registry.example/baron_sso/backend:v1.2606.ab12`는 `baron-sso/v1.2606.ab12/backend.v1.2606.ab12.tar.zst`로 저장한다. ## Manifest -`manifest.json`에는 다음 정보를 기록한다. +`manifest..json`에는 다음 정보를 기록한다. - archive format: `docker-save-zstd` - 원본 `image_ref` @@ -83,14 +83,15 @@ Registry hostname은 저장 경로에서 제외한다. 예를 들어 `registry.e - Docker image id - Git commit - archive 파일명, 크기, sha256 +- 서비스별 archive 정보 (`images.`) - WORKS Drive remote path - 복원 명령 예시 복원은 다음 흐름으로 처리한다. ```bash -sha256sum -c image.tar.zst.sha256 -zstd -d -c image.tar.zst | docker load +sha256sum -c backend.v1.2606.ab12.sha256 +zstd -d -c backend.v1.2606.ab12.tar.zst | docker load ``` ## 업로드 CLI @@ -98,7 +99,7 @@ zstd -d -c image.tar.zst | docker load 로컬 컨테이너를 먼저 이미지로 commit한 뒤 업로드하려면 다음처럼 실행한다. ```bash -WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR=docker-build-image \ +WORKS_DRIVE_DOCKER_IMAGE_DIR=baron-sso \ WORKS_DOCKER_COMMIT_CONTAINER=baron_backend \ DOCKER_IMAGE_REF=registry.example/baron_sso/backend:v1.2606.ab12 \ scripts/docker-image/upload_works_drive.sh @@ -111,11 +112,11 @@ DOCKER_IMAGE_REF=registry.example/baron_sso/backend:v1.2606.ab12 \ scripts/docker-image/upload_works_drive.sh ``` -실제 업로드에는 기존 백업 업로드와 같은 WORKS Drive 인증 변수를 사용한다. +실제 업로드에는 기존 백업 업로드와 같은 WORKS Drive 인증 변수를 사용하되, Docker image archive 대상 drive/folder는 백업 변수와 분리한다. - `WORKS_DRIVE_TARGET=sharedrive` -- `WORKS_DRIVE_SHARED_DRIVE_ID` 또는 `WORKS_SHAREDRIVE_ID` -- 선택: `WORKS_DRIVE_PARENT_FILE_ID` +- `WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID` +- 선택: `WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID` - `WORKS_DRIVE_ACCESS_TOKEN`, `WORKS_DRIVE_ACCESS_TOKEN_FILE`, `WORKS_DRIVE_ACCESS_TOKEN_CMD`, `WORKS_DRIVE_OAUTH_REFRESH_TOKEN`, 또는 서비스 계정 OAuth 변수 업로드 전 packaging만 확인하려면 다음을 사용한다. @@ -133,9 +134,9 @@ dry-run도 실제 `docker save`, `zstd`, checksum, manifest 생성을 수행한 WORKS Drive에서 다음 세 파일을 같은 로컬 디렉터리로 내려받은 뒤 검증한다. ```text -image.tar.zst -image.tar.zst.sha256 -manifest.json +backend.v1.2606.ab12.tar.zst +backend.v1.2606.ab12.sha256 +manifest.v1.2606.ab12.json ``` checksum, manifest, zstd stream 무결성만 확인하려면 다음을 실행한다. @@ -160,13 +161,20 @@ scripts/docker-image/verify_archive.sh /path/to/downloaded/archive 검증은 다음 조건을 모두 확인한다. -- `image.tar.zst.sha256` checksum 성공 -- `manifest.json`의 `schema_version=1`, `format=docker-save-zstd` +- `..sha256` checksum 성공 +- `manifest..json`의 `schema_version=1`, `format=docker-save-zstd` - manifest의 archive 파일명, sha256, size와 실제 파일 일치 - `zstd -t` 무결성 성공 - 선택적으로 `docker load` 성공 -현재 repo에는 WORKS Drive API에서 파일을 자동 다운로드하는 CLI는 없다. 따라서 자동 다운로드 스크립트를 만들기 전까지는 WORKS Drive UI 또는 운영자가 승인한 API 도구로 세 파일을 내려받고, 위 검증 CLI로 복원 가능성을 확인한다. +현재 repo의 `scripts/docker-image/download_works_drive.sh`는 `WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID`와 `IMAGE_TAG`를 사용해 `baron-sso//`의 archive, checksum, manifest를 내려받고 checksum/manifest 검증 후 `docker load`를 수행한다. + +```bash +WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID=@2001000000547281 \ +WORKS_DRIVE_ACCESS_TOKEN= \ +IMAGE_TAG=v1.2606.ab12 \ +scripts/docker-image/download_works_drive.sh +``` API로 다운로드할 때는 대상 archive 폴더의 children을 조회해 각 파일의 `fileId`를 얻은 뒤 다음 endpoint를 호출한다. @@ -179,7 +187,7 @@ GET /v1.0/sharedrives//files//download ```bash curl -sS -L --location-trusted \ -H "Authorization: Bearer " \ - -o image.tar.zst \ + -o backend.v1.2606.ab12.tar.zst \ "https://www.worksapis.com/v1.0/sharedrives//files//download" ``` @@ -224,5 +232,5 @@ WORKS Drive archive는 Action에서 로컬로 빌드된 이미지를 `docker sav - 이 구조는 `docker push`/`docker pull`과 호환되는 Registry backend가 아니다. - layer deduplication이 없으므로 같은 기반 이미지가 반복 저장된다. -- 배포 전에는 반드시 `image.tar.zst.sha256` 검증 후 `docker load`를 수행해야 한다. +- 배포 전에는 반드시 `..sha256` 검증 후 `docker load`를 수행해야 한다. - tag 없는 image ref와 digest-only image ref는 지원하지 않는다. diff --git a/scripts/deploy/build_image_deploy_bundle.sh b/scripts/deploy/build_image_deploy_bundle.sh index 2ab6da17..0b0b41da 100755 --- a/scripts/deploy/build_image_deploy_bundle.sh +++ b/scripts/deploy/build_image_deploy_bundle.sh @@ -29,7 +29,6 @@ require_env ADMINFRONT_URL require_env DEVFRONT_URL require_env ORGFRONT_URL require_env VITE_OIDC_AUTHORITY -require_env HARBOR_HOSTNAME if ! printf '%s' "$IMAGE_TAG" | grep -Eq '^v[0-9]+\.[0-9]{4}\.[0-9a-f]{4}$'; then die "IMAGE_TAG must look like vX.YYMM.ab12 (got: $IMAGE_TAG)" @@ -59,6 +58,10 @@ compose_template="${IMAGE_DEPLOY_COMPOSE_TEMPLATE:-$repo_root/deploy/templates/d rm -rf "$bundle_dir" TARGET_DIR="$bundle_dir" bash "$repo_root/deploy/create-instance.sh" "$instance_name" "$port_prefix" cp "$compose_template" "$bundle_dir/docker-compose.yml" +mkdir -p "$bundle_dir/scripts/docker-image" "$bundle_dir/scripts/backup/lib" +cp "$repo_root/scripts/docker-image/download_works_drive.sh" "$bundle_dir/scripts/docker-image/download_works_drive.sh" +cp "$repo_root/scripts/backup/lib/common.sh" "$bundle_dir/scripts/backup/lib/common.sh" +chmod +x "$bundle_dir/scripts/docker-image/download_works_drive.sh" sed "s/{{BACKEND_PORT}}/${IMAGE_DEPLOY_BACKEND_PORT}/g" \ "$repo_root/deploy/templates/gateway/nginx.conf" >"$bundle_dir/gateway/nginx.conf" diff --git a/scripts/deploy/upload_and_run_image_deploy.sh b/scripts/deploy/upload_and_run_image_deploy.sh index 615a3263..3dde6cc5 100755 --- a/scripts/deploy/upload_and_run_image_deploy.sh +++ b/scripts/deploy/upload_and_run_image_deploy.sh @@ -15,25 +15,78 @@ require_env IMAGE_DEPLOY_BUNDLE_FILE require_env DEPLOY_HOST require_env DEPLOY_USER require_env DEPLOY_PATH -require_env HARBOR_ENDPOINT -require_env HARBOR_ROBOT_ACCOUNT -require_env HARBOR_ROBOT_KEY +require_env WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID [[ -f "$IMAGE_DEPLOY_BUNDLE_FILE" ]] || die "bundle file not found: $IMAGE_DEPLOY_BUNDLE_FILE" +resolve_works_drive_access_token() { + if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN:-}" ]]; then + printf '%s\n' "$WORKS_DRIVE_ACCESS_TOKEN" + return + fi + + if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN_INPUT:-}" ]]; then + printf '%s\n' "$WORKS_DRIVE_ACCESS_TOKEN_INPUT" + return + fi + + if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN_FILE:-}" ]]; then + [[ -f "$WORKS_DRIVE_ACCESS_TOKEN_FILE" ]] || die "WORKS_DRIVE_ACCESS_TOKEN_FILE not found: $WORKS_DRIVE_ACCESS_TOKEN_FILE" + sed -n '1p' "$WORKS_DRIVE_ACCESS_TOKEN_FILE" + return + fi + + if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN_CMD:-}" ]]; then + sh -c "$WORKS_DRIVE_ACCESS_TOKEN_CMD" + 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." +} + remote_bundle="/tmp/baron-sso-image-deploy-$(date -u '+%Y%m%d%H%M%S').tgz" +works_drive_access_token="$(resolve_works_drive_access_token)" ssh-keyscan -H "$DEPLOY_HOST" >>~/.ssh/known_hosts scp "$IMAGE_DEPLOY_BUNDLE_FILE" "${DEPLOY_USER}@${DEPLOY_HOST}:${remote_bundle}" -echo "$HARBOR_ROBOT_KEY" | ssh "${DEPLOY_USER}@${DEPLOY_HOST}" \ +printf '%s\n' "$works_drive_access_token" | ssh "${DEPLOY_USER}@${DEPLOY_HOST}" \ "set -euo pipefail; \ + read -r works_drive_access_token; \ mkdir -p '${DEPLOY_PATH}'; \ tar -xzf '${remote_bundle}' -C '${DEPLOY_PATH}'; \ cd '${DEPLOY_PATH}'; \ chmod 600 .env; \ docker network inspect traefik-public >/dev/null 2>&1 || docker network create traefik-public; \ - docker login '${HARBOR_ENDPOINT}' -u '${HARBOR_ROBOT_ACCOUNT}' --password-stdin; \ - docker compose --env-file .env -f docker-compose.yml pull; \ + export WORKS_DRIVE_ACCESS_TOKEN=\"\${works_drive_access_token}\"; \ + export WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID='${WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID}'; \ + 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}'; \ + scripts/docker-image/download_works_drive.sh; \ 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/scripts/docker-image/download_works_drive.sh b/scripts/docker-image/download_works_drive.sh new file mode 100755 index 00000000..6fc5fef4 --- /dev/null +++ b/scripts/docker-image/download_works_drive.sh @@ -0,0 +1,185 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "$script_dir/../.." && pwd)" +source "$repo_root/scripts/backup/lib/common.sh" + +dotenv_value() { + local key="$1" + local env_file="${WORKS_DOCKER_IMAGE_ENV_FILE:-.env}" + + [[ -f "$env_file" ]] || return 0 + sed -n "s/^${key}=//p" "$env_file" | tail -n 1 +} + +urlencode_path() { + jq -nr --arg value "$1" '$value|@uri' +} + +split_curl_response() { + local response="$1" + local __body_var="$2" + local __status_var="$3" + local status + local body + + status="$(tail -n 1 <<<"$response")" + if [[ "$status" =~ ^[0-9][0-9][0-9]$ ]]; then + body="$(sed '$d' <<<"$response")" + else + status="200" + body="$response" + fi + + printf -v "$__body_var" '%s' "$body" + printf -v "$__status_var" '%s' "$status" +} + +redact_for_log() { + sed -E 's/("(access_token|refresh_token|assertion|client_secret|Authorization)"[[:space:]]*:[[:space:]]*)"[^"]*"/\1"REDACTED"/Ig' +} + +resolve_files_endpoint() { + local parent_file_id="${1:-}" + local encoded_drive_id + + encoded_drive_id="$(urlencode_path "$drive_id")" + if [[ -n "$parent_file_id" ]]; then + printf '%s/v1.0/sharedrives/%s/files/%s\n' "$api_base_url" "$encoded_drive_id" "$(urlencode_path "$parent_file_id")" + else + printf '%s/v1.0/sharedrives/%s/files\n' "$api_base_url" "$encoded_drive_id" + fi +} + +list_children() { + local parent_file_id="${1:-}" + local endpoint + local response + local response_body + local http_status + + endpoint="$(resolve_files_endpoint "$parent_file_id")/children" + response="$("$curl_bin" -sS -w $'\n%{http_code}' \ + -H "Authorization: Bearer $access_token" \ + "$endpoint")" + split_curl_response "$response" response_body http_status + + if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then + backup_die "WORKS folder list request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)" + fi + + printf '%s\n' "$response_body" +} + +find_child_id() { + local parent_file_id="$1" + local child_name="$2" + local expected_type="${3:-}" + local children_json + + children_json="$(list_children "$parent_file_id")" + jq -er --arg name "$child_name" --arg expectedType "$expected_type" ' + [ + (.files // .children // .items // [])[] + | select((.fileName // .name) == $name) + | select( + $expectedType == "" + or (((.fileType // .type // "") | ascii_downcase) == ($expectedType | ascii_downcase)) + ) + | .fileId // .id + ][0] // empty + ' <<<"$children_json" 2>/dev/null || true +} + +resolve_folder_path() { + local path="$1" + local parent_file_id="${WORKS_DRIVE_DOCKER_IMAGE_PARENT_FILE_ID:-}" + local component + local folder_id + + IFS='/' read -r -a components <<<"$path" + for component in "${components[@]}"; do + [[ -n "$component" ]] || continue + folder_id="$(find_child_id "$parent_file_id" "$component" "folder")" + [[ -n "$folder_id" ]] || backup_die "WORKS Drive folder not found: ${path}" + parent_file_id="$folder_id" + done + + printf '%s\n' "$parent_file_id" +} + +download_file() { + local file_id="$1" + local output_file="$2" + local endpoint + + endpoint="$(resolve_files_endpoint "$file_id")/download" + "$curl_bin" -fL -sS \ + --location-trusted \ + -H "Authorization: Bearer $access_token" \ + -o "$output_file" \ + "$endpoint" +} + +load_image_archive() { + local archive_file="$1" + + backup_log "Loading Docker image archive: $(basename "$archive_file")" + zstd -dc "$archive_file" | docker load >/dev/null +} + +image_tag="${IMAGE_TAG:-$(dotenv_value IMAGE_TAG)}" +drive_id="${WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID:-${WORKS_DRIVE_SHARED_DRIVE_ID:-}}" +access_token="${WORKS_DRIVE_ACCESS_TOKEN:-}" +api_base_url="${WORKS_ADMIN_API_BASE_URL:-https://www.worksapis.com}" +curl_bin="${WORKS_DRIVE_CURL_BIN:-curl}" +image_root_dir="${WORKS_DRIVE_DOCKER_IMAGE_DIR:-${WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR:-baron-sso}}" +download_root="${WORKS_DOCKER_IMAGE_DOWNLOAD_DIR:-/tmp/baron-sso-docker-image-download}" +image_list="${WORKS_DOCKER_IMAGE_NAMES:-backend userfront adminfront devfront orgfront}" + +[[ -n "$image_tag" ]] || backup_die "IMAGE_TAG is required." +[[ -n "$drive_id" ]] || backup_die "WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID is required." +[[ -n "$access_token" ]] || backup_die "WORKS_DRIVE_ACCESS_TOKEN is required." + +backup_require_command jq +backup_require_command sha256sum +backup_require_command zstd +backup_require_command docker +backup_require_command "$curl_bin" + +# Normalized remote path: baron-sso/${IMAGE_TAG}/${image}.${IMAGE_TAG}.tar.zst +remote_path="${image_root_dir}/${image_tag}" +target_folder_id="$(resolve_folder_path "$remote_path")" +artifact_dir="${download_root}/${remote_path}" +mkdir -p "$artifact_dir" + +for image in $image_list; do + archive_name="${image}.${image_tag}.tar.zst" + checksum_name="${image}.${image_tag}.sha256" + manifest_name="manifest.${image_tag}.json" + + archive_id="$(find_child_id "$target_folder_id" "$archive_name")" + checksum_id="$(find_child_id "$target_folder_id" "$checksum_name")" + manifest_id="$(find_child_id "$target_folder_id" "$manifest_name")" + + [[ -n "$archive_id" ]] || backup_die "WORKS Drive image archive not found: ${remote_path}/${archive_name}" + [[ -n "$checksum_id" ]] || backup_die "WORKS Drive image checksum not found: ${remote_path}/${checksum_name}" + [[ -n "$manifest_id" ]] || backup_die "WORKS Drive image manifest not found: ${remote_path}/${manifest_name}" + + download_file "$archive_id" "$artifact_dir/$archive_name" + download_file "$checksum_id" "$artifact_dir/$checksum_name" + download_file "$manifest_id" "$artifact_dir/$manifest_name" + + ( + cd "$artifact_dir" + sha256sum -c "$checksum_name" >/dev/null + ) + manifest_sha256="$(jq -er --arg image "$image" '.images[$image].archive.sha256 // .archive.sha256' "$artifact_dir/$manifest_name")" + actual_sha256="$(sha256sum "$artifact_dir/$archive_name" | awk '{print $1}')" + [[ "$manifest_sha256" == "$actual_sha256" ]] || backup_die "manifest sha256 mismatch for $archive_name" + + load_image_archive "$artifact_dir/$archive_name" +done + +backup_log "Loaded WORKS Drive Docker image archives from ${remote_path}" diff --git a/scripts/docker-image/upload_works_drive.sh b/scripts/docker-image/upload_works_drive.sh index 9eb2310b..324a132f 100755 --- a/scripts/docker-image/upload_works_drive.sh +++ b/scripts/docker-image/upload_works_drive.sh @@ -68,7 +68,7 @@ image_ref="${DOCKER_IMAGE_REF:-${IMAGE_REF:-}}" commit_container="${WORKS_DOCKER_COMMIT_CONTAINER:-${DOCKER_COMMIT_CONTAINER:-}}" archive_root="${WORKS_DOCKER_IMAGE_ARCHIVE_DIR:-/tmp/baron-sso-docker-image-upload}" -image_root_dir="${WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR:-docker-build-image}" +image_root_dir="${WORKS_DRIVE_DOCKER_IMAGE_DIR:-${WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR:-baron-sso}}" dry_run="${WORKS_DRIVE_DRY_RUN:-false}" target="${WORKS_DRIVE_TARGET:-sharedrive}" api_base_url="${WORKS_ADMIN_API_BASE_URL:-https://www.worksapis.com}" @@ -504,14 +504,16 @@ derive_repository_and_tag() { derive_repository_and_tag "$image_ref" -remote_path="${image_root_dir}/${image_repository}/${image_tag}" -artifact_dir="${archive_root}/${image_repository}/${image_tag}" +image_name="${image_repository##*/}" +release_repository="${image_root_dir}" +remote_path="${release_repository}/${image_tag}" +artifact_dir="${archive_root}/${release_repository}/${image_tag}" mkdir -p "$artifact_dir" -tar_file="$artifact_dir/image.tar" -archive_file="$artifact_dir/image.tar.zst" -checksum_file="$artifact_dir/image.tar.zst.sha256" -manifest_file="$artifact_dir/manifest.json" +tar_file="$artifact_dir/${image_name}.${image_tag}.tar" +archive_file="$artifact_dir/${image_name}.${image_tag}.tar.zst" +checksum_file="$artifact_dir/${image_name}.${image_tag}.sha256" +manifest_file="$artifact_dir/manifest.${image_tag}.json" upload_report_file="$artifact_dir/works-upload.json" rm -f "$tar_file" "$archive_file" "$checksum_file" "$manifest_file" "$upload_report_file" @@ -535,36 +537,77 @@ image_id="$(docker image inspect "$image_ref" --format '{{.Id}}' 2>/dev/null || archive_size="$(stat -c '%s' "$archive_file")" git_commit="$(backup_git_commit "$repo_root")" -jq -n \ - --arg createdAt "$(backup_utc_now)" \ - --arg imageRef "$image_ref" \ - --arg repository "$image_repository" \ - --arg tag "$image_tag" \ - --arg sourceContainer "$commit_container" \ - --arg imageId "$image_id" \ - --arg gitCommit "$git_commit" \ - --arg remotePath "$remote_path" \ - --arg archiveFile "$(basename "$archive_file")" \ - --arg archiveSha256 "$archive_sha256" \ - --argjson archiveSize "$archive_size" \ - '{ - schema_version: 1, - format: "docker-save-zstd", - created_at: $createdAt, +manifest_tmp_file="${manifest_file}.tmp" +manifest_jq_filter=' + def image_entry: { image_ref: $imageRef, repository: $repository, - tag: $tag, + image_name: $imageName, source_container: $sourceContainer, docker_image_id: $imageId, - git_commit: $gitCommit, - remote_path: $remotePath, - restore_command: ("zstd -d -c " + $archiveFile + " | docker load"), archive: { file_name: $archiveFile, size_bytes: $archiveSize, sha256: $archiveSha256 + }, + restore_command: ("zstd -d -c " + $archiveFile + " | docker load") + }; + .schema_version = 1 + | .format = "docker-save-zstd" + | .created_at = (.created_at // $createdAt) + | .updated_at = $createdAt + | .image_ref = $imageRef + | .repository = $repository + | .release_repository = $releaseRepository + | .image_name = $imageName + | .tag = $tag + | .source_container = $sourceContainer + | .docker_image_id = $imageId + | .git_commit = $gitCommit + | .remote_path = $remotePath + | .restore_command = ("zstd -d -c " + $archiveFile + " | docker load") + | .archive = { + file_name: $archiveFile, + size_bytes: $archiveSize, + sha256: $archiveSha256 } - }' >"$manifest_file" + | .images = ((.images // {}) + {($imageName): image_entry}) +' + +if [[ -f "$manifest_file" ]]; then + jq \ + --arg createdAt "$(backup_utc_now)" \ + --arg imageRef "$image_ref" \ + --arg repository "$image_repository" \ + --arg releaseRepository "$release_repository" \ + --arg imageName "$image_name" \ + --arg tag "$image_tag" \ + --arg sourceContainer "$commit_container" \ + --arg imageId "$image_id" \ + --arg gitCommit "$git_commit" \ + --arg remotePath "$remote_path" \ + --arg archiveFile "$(basename "$archive_file")" \ + --arg archiveSha256 "$archive_sha256" \ + --argjson archiveSize "$archive_size" \ + "$manifest_jq_filter" "$manifest_file" >"$manifest_tmp_file" +else + jq -n \ + --arg createdAt "$(backup_utc_now)" \ + --arg imageRef "$image_ref" \ + --arg repository "$image_repository" \ + --arg releaseRepository "$release_repository" \ + --arg imageName "$image_name" \ + --arg tag "$image_tag" \ + --arg sourceContainer "$commit_container" \ + --arg imageId "$image_id" \ + --arg gitCommit "$git_commit" \ + --arg remotePath "$remote_path" \ + --arg archiveFile "$(basename "$archive_file")" \ + --arg archiveSha256 "$archive_sha256" \ + --argjson archiveSize "$archive_size" \ + "$manifest_jq_filter" >"$manifest_tmp_file" +fi +mv "$manifest_tmp_file" "$manifest_file" upload_files=("$archive_file" "$checksum_file" "$manifest_file") diff --git a/scripts/docker-image/verify_archive.sh b/scripts/docker-image/verify_archive.sh index ee92c7f4..995528f1 100755 --- a/scripts/docker-image/verify_archive.sh +++ b/scripts/docker-image/verify_archive.sh @@ -17,6 +17,11 @@ backup_require_command stat backup_require_command zstd manifest_file="$archive_dir/manifest.json" +mapfile -t versioned_manifest_files < <(find "$archive_dir" -maxdepth 1 -type f -name 'manifest.*.json' | sort) +if [[ "${#versioned_manifest_files[@]}" -gt 0 ]]; then + [[ "${#versioned_manifest_files[@]}" -eq 1 ]] || backup_die "archive directory must contain exactly one manifest.*.json file." + manifest_file="${versioned_manifest_files[0]}" +fi backup_require_path "$manifest_file" schema_version="$(jq -er '.schema_version' "$manifest_file")" @@ -33,6 +38,9 @@ manifest_size="$(jq -er '.archive.size_bytes' "$manifest_file")" archive_file="$archive_dir/$archive_name" checksum_file="$archive_dir/${archive_name}.sha256" +if [[ ! -f "$checksum_file" && "$archive_name" == *.tar.zst ]]; then + checksum_file="$archive_dir/${archive_name%.tar.zst}.sha256" +fi backup_require_path "$archive_file" backup_require_path "$checksum_file" diff --git a/scripts/test_docker_image_archive_verify.sh b/scripts/test_docker_image_archive_verify.sh index 1b7e5600..7b0c4867 100755 --- a/scripts/test_docker_image_archive_verify.sh +++ b/scripts/test_docker_image_archive_verify.sh @@ -28,19 +28,19 @@ require_command jq require_command sha256sum require_command zstd -artifact_dir="$tmp_root/baron_sso/backend/v1.2606.ab12" +artifact_dir="$tmp_root/baron-sso/v1.2606.ab12" mkdir -p "$artifact_dir" -printf 'docker image archive smoke\n' >"$artifact_dir/image.tar" -zstd -q -f -o "$artifact_dir/image.tar.zst" "$artifact_dir/image.tar" -rm -f "$artifact_dir/image.tar" +printf 'docker image archive smoke\n' >"$artifact_dir/backend.v1.2606.ab12.tar" +zstd -q -f -o "$artifact_dir/backend.v1.2606.ab12.tar.zst" "$artifact_dir/backend.v1.2606.ab12.tar" +rm -f "$artifact_dir/backend.v1.2606.ab12.tar" -archive_sha256="$(sha256sum "$artifact_dir/image.tar.zst" | awk '{print $1}')" -archive_size="$(wc -c <"$artifact_dir/image.tar.zst" | tr -d ' ')" -printf '%s image.tar.zst\n' "$archive_sha256" >"$artifact_dir/image.tar.zst.sha256" +archive_sha256="$(sha256sum "$artifact_dir/backend.v1.2606.ab12.tar.zst" | awk '{print $1}')" +archive_size="$(wc -c <"$artifact_dir/backend.v1.2606.ab12.tar.zst" | tr -d ' ')" +printf '%s backend.v1.2606.ab12.tar.zst\n' "$archive_sha256" >"$artifact_dir/backend.v1.2606.ab12.sha256" jq -n \ - --arg remotePath "docker-build-image/baron_sso/backend/v1.2606.ab12" \ + --arg remotePath "baron-sso/v1.2606.ab12" \ --arg archiveSha256 "$archive_sha256" \ --argjson archiveSize "$archive_size" \ '{ @@ -48,33 +48,35 @@ jq -n \ format: "docker-save-zstd", image_ref: "reg.hmac.kr/baron_sso/backend:v1.2606.ab12", repository: "baron_sso/backend", + release_repository: "baron-sso", + image_name: "backend", tag: "v1.2606.ab12", remote_path: $remotePath, archive: { - file_name: "image.tar.zst", + file_name: "backend.v1.2606.ab12.tar.zst", size_bytes: $archiveSize, sha256: $archiveSha256 } - }' >"$artifact_dir/manifest.json" + }' >"$artifact_dir/manifest.v1.2606.ab12.json" "$verify_script" "$artifact_dir" >/dev/null bad_checksum_dir="$tmp_root/bad-checksum" cp -R "$artifact_dir" "$bad_checksum_dir" -printf '0000000000000000000000000000000000000000000000000000000000000000 image.tar.zst\n' >"$bad_checksum_dir/image.tar.zst.sha256" +printf '0000000000000000000000000000000000000000000000000000000000000000 backend.v1.2606.ab12.tar.zst\n' >"$bad_checksum_dir/backend.v1.2606.ab12.sha256" assert_fails "$verify_script" "$bad_checksum_dir" bad_manifest_dir="$tmp_root/bad-manifest" cp -R "$artifact_dir" "$bad_manifest_dir" jq '.archive.sha256 = "1111111111111111111111111111111111111111111111111111111111111111"' \ - "$bad_manifest_dir/manifest.json" >"$bad_manifest_dir/manifest.json.tmp" -mv "$bad_manifest_dir/manifest.json.tmp" "$bad_manifest_dir/manifest.json" + "$bad_manifest_dir/manifest.v1.2606.ab12.json" >"$bad_manifest_dir/manifest.v1.2606.ab12.json.tmp" +mv "$bad_manifest_dir/manifest.v1.2606.ab12.json.tmp" "$bad_manifest_dir/manifest.v1.2606.ab12.json" assert_fails "$verify_script" "$bad_manifest_dir" bad_archive_dir="$tmp_root/bad-archive" cp -R "$artifact_dir" "$bad_archive_dir" -printf 'not a zstd stream\n' >"$bad_archive_dir/image.tar.zst" -sha256sum "$bad_archive_dir/image.tar.zst" | awk '{print $1 " image.tar.zst"}' >"$bad_archive_dir/image.tar.zst.sha256" +printf 'not a zstd stream\n' >"$bad_archive_dir/backend.v1.2606.ab12.tar.zst" +sha256sum "$bad_archive_dir/backend.v1.2606.ab12.tar.zst" | awk '{print $1 " backend.v1.2606.ab12.tar.zst"}' >"$bad_archive_dir/backend.v1.2606.ab12.sha256" assert_fails "$verify_script" "$bad_archive_dir" echo "docker image archive verification checks passed" diff --git a/test/production_image_workflows_policy_test.sh b/test/production_image_workflows_policy_test.sh index 3f2630d3..ef1868bc 100644 --- a/test/production_image_workflows_policy_test.sh +++ b/test/production_image_workflows_policy_test.sh @@ -9,6 +9,7 @@ deploy_workflow="$repo_root/.gitea/workflows/production_image_deploy.yml" image_compose="$repo_root/deploy/templates/docker-compose.images.yaml" bundle_script="$repo_root/scripts/deploy/build_image_deploy_bundle.sh" remote_deploy_script="$repo_root/scripts/deploy/upload_and_run_image_deploy.sh" +works_image_download_script="$repo_root/scripts/docker-image/download_works_drive.sh" fail() { echo "$1" >&2 @@ -22,6 +23,7 @@ fail() { [[ -f "$image_compose" ]] || fail "image-based production compose template must exist." [[ -f "$bundle_script" ]] || fail "shared image deployment bundle script must exist." [[ -f "$remote_deploy_script" ]] || fail "shared image remote deploy script must exist." +[[ -f "$works_image_download_script" ]] || fail "shared WORKS Drive image download script must exist." grep -Fq "name: Publish Baron SSO Images" "$publish_workflow" \ || fail "publish workflow must have the shared stage/production name." @@ -58,8 +60,10 @@ grep -Fq "WORKS_DRIVE_OAUTH_CLIENT_SECRET: \${{ secrets.WORKS_OAUTH_CLIENT_SECRE || fail "publish workflow must use the Gitea-compatible WORKS OAuth client secret name." grep -Fq "WORKS_DRIVE_OAUTH_REFRESH_TOKEN: \${{ secrets.WORKS_DRIVE_REFRESH_TOKEN }}" "$publish_workflow" \ || fail "publish workflow must support WORKS Drive refresh-token auth." -grep -Fq "WORKS_DRIVE_PARENT_FILE_ID" "$publish_workflow" \ - || fail "publish workflow must target a WORKS Drive folder." +grep -Fq "WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID: \${{ vars.WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID }}" "$publish_workflow" \ + || fail "publish workflow must use the Docker-image-specific WORKS Drive ID variable." +grep -Fq 'WORKS_DRIVE_SHARED_DRIVE_ID="${WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID}"' "$publish_workflow" \ + || fail "publish workflow must map WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID into the shared upload script." grep -Fq "Resolve WORKS Drive access token" "$publish_workflow" \ || fail "publish workflow must resolve a short-lived WORKS Drive access token before uploads." grep -Fq "WORKS_DRIVE_ACCESS_TOKEN_INPUT" "$publish_workflow" \ @@ -90,6 +94,8 @@ grep -Fq "scripts/deploy/build_image_deploy_bundle.sh" "$staging_deploy_workflow || fail "staging deploy workflow must use the shared bundle script." grep -Fq "scripts/deploy/upload_and_run_image_deploy.sh" "$staging_deploy_workflow" \ || 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 "name: Deploy Baron SSO Production Images" "$deploy_workflow" \ || fail "deploy workflow must have the expected name." @@ -109,14 +115,23 @@ grep -Fq "scripts/deploy/build_image_deploy_bundle.sh" "$deploy_workflow" \ || fail "production deploy workflow must use the shared bundle script." 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 "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 "docker compose --env-file .env -f docker-compose.yml pull" "$remote_deploy_script" \ - || fail "shared remote deploy script must pull the requested image version before running." +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 "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." +fi if grep -Eq 'docker (build|commit)' "$staging_deploy_workflow" "$deploy_workflow"; then fail "staging/production deploy workflows must never build or commit images remotely." diff --git a/test/works_drive_docker_image_download_policy_test.sh b/test/works_drive_docker_image_download_policy_test.sh new file mode 100644 index 00000000..72f5b715 --- /dev/null +++ b/test/works_drive_docker_image_download_policy_test.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +script="$repo_root/scripts/docker-image/download_works_drive.sh" +tmp_root="$(mktemp -d)" + +cleanup() { + rm -rf "$tmp_root" +} +trap cleanup EXIT INT TERM + +fail() { + echo "$1" >&2 + exit 1 +} + +[[ -x "$script" ]] || fail "download script must exist and be executable." +grep -Fq 'baron-sso/${IMAGE_TAG}/${image}.${IMAGE_TAG}.tar.zst' "$script" \ + || fail "download script must document the normalized archive path." +grep -Fq -- '--location-trusted' "$script" \ + || fail "download script must preserve Authorization across WORKS download redirects." + +source_dir="$tmp_root/source" +mkdir -p "$source_dir" +printf 'backend image archive payload\n' >"$source_dir/backend.txt" +tar -C "$source_dir" -cf "$tmp_root/backend.v1.2606.ab12.tar" backend.txt +zstd -q -f -o "$tmp_root/backend.v1.2606.ab12.tar.zst" "$tmp_root/backend.v1.2606.ab12.tar" +archive_sha256="$(sha256sum "$tmp_root/backend.v1.2606.ab12.tar.zst" | awk '{print $1}')" +archive_size="$(wc -c <"$tmp_root/backend.v1.2606.ab12.tar.zst" | tr -d ' ')" +printf '%s backend.v1.2606.ab12.tar.zst\n' "$archive_sha256" >"$tmp_root/backend.v1.2606.ab12.sha256" +jq -n \ + --arg sha "$archive_sha256" \ + --argjson size "$archive_size" \ + '{ + schema_version: 1, + format: "docker-save-zstd", + image_ref: "baron_sso/backend:v1.2606.ab12", + repository: "baron_sso/backend", + release_repository: "baron-sso", + image_name: "backend", + tag: "v1.2606.ab12", + remote_path: "baron-sso/v1.2606.ab12", + archive: { + file_name: "backend.v1.2606.ab12.tar.zst", + sha256: $sha, + size_bytes: $size + }, + images: { + backend: { + archive: { + file_name: "backend.v1.2606.ab12.tar.zst", + sha256: $sha, + size_bytes: $size + } + } + } + }' >"$tmp_root/manifest.v1.2606.ab12.json" + +curl_log="$tmp_root/curl.log" +docker_log="$tmp_root/docker.log" +fake_curl="$tmp_root/curl" +cat >"$fake_curl" <<'SH' +#!/usr/bin/env bash +set -euo pipefail + +printf '%s\n' "$*" >>"$FAKE_CURL_LOG" + +output_file="" +args=("$@") +for ((i = 0; i < ${#args[@]}; i += 1)); do + if [[ "${args[$i]}" == "-o" ]]; then + output_file="${args[$((i + 1))]}" + fi +done +url="${args[-1]}" + +if [[ -n "$output_file" ]]; then + case "$url" in + */files/backend-archive/download) cp "$FAKE_WORKS_SOURCE/backend.v1.2606.ab12.tar.zst" "$output_file" ;; + */files/backend-checksum/download) cp "$FAKE_WORKS_SOURCE/backend.v1.2606.ab12.sha256" "$output_file" ;; + */files/backend-manifest/download) cp "$FAKE_WORKS_SOURCE/manifest.v1.2606.ab12.json" "$output_file" ;; + *) echo "unexpected download URL: $url" >&2; exit 2 ;; + esac + exit 0 +fi + +case "$url" in + https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/children) + printf '{"files":[{"fileId":"baron-sso-id","fileName":"baron-sso","fileType":"FOLDER"}]}\n200\n' + ;; + https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/baron-sso-id/children) + printf '{"files":[{"fileId":"tag-id","fileName":"v1.2606.ab12","fileType":"FOLDER"}]}\n200\n' + ;; + https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/tag-id/children) + printf '{"files":[{"fileId":"backend-archive","fileName":"backend.v1.2606.ab12.tar.zst","fileType":"FILE"},{"fileId":"backend-checksum","fileName":"backend.v1.2606.ab12.sha256","fileType":"FILE"},{"fileId":"backend-manifest","fileName":"manifest.v1.2606.ab12.json","fileType":"FILE"}]}\n200\n' + ;; + *) + echo "unexpected list URL: $url" >&2 + exit 2 + ;; +esac +SH +chmod +x "$fake_curl" + +fake_bin="$tmp_root/bin" +mkdir -p "$fake_bin" +cat >"$fake_bin/docker" <<'SH' +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$1" == "load" ]]; then + bytes="$(wc -c | tr -d ' ')" + printf 'docker load bytes=%s\n' "$bytes" >>"$FAKE_DOCKER_LOG" + exit 0 +fi + +echo "unexpected docker command: $*" >&2 +exit 2 +SH +chmod +x "$fake_bin/docker" + +PATH="$fake_bin:$PATH" \ +FAKE_CURL_LOG="$curl_log" \ +FAKE_DOCKER_LOG="$docker_log" \ +FAKE_WORKS_SOURCE="$tmp_root" \ +WORKS_DRIVE_CURL_BIN="$fake_curl" \ +WORKS_DRIVE_ACCESS_TOKEN="test-access-token" \ +WORKS_DRIVE_DOCKER_IMAGE_DRIVE_ID="shared-drive-1" \ +WORKS_DOCKER_IMAGE_NAMES="backend" \ +WORKS_DOCKER_IMAGE_DOWNLOAD_DIR="$tmp_root/downloaded" \ +IMAGE_TAG="v1.2606.ab12" \ + "$script" >/dev/null + +grep -Fq "sharedrives/shared-drive-1/files/children" "$curl_log" \ + || fail "download script must list the shared drive root." +grep -Fq "sharedrives/shared-drive-1/files/baron-sso-id/children" "$curl_log" \ + || fail "download script must resolve the baron-sso folder." +grep -Fq "sharedrives/shared-drive-1/files/tag-id/children" "$curl_log" \ + || fail "download script must resolve the image tag folder." +grep -Fq "files/backend-archive/download" "$curl_log" \ + || fail "download script must download the normalized backend archive." +grep -Fq "docker load bytes=" "$docker_log" \ + || fail "download script must load the downloaded archive into Docker." + +echo "OK: WORKS Drive Docker image archive download flow resolves, verifies, and loads image artifacts" diff --git a/test/works_drive_docker_image_upload_policy_test.sh b/test/works_drive_docker_image_upload_policy_test.sh index 550671e2..ba957779 100755 --- a/test/works_drive_docker_image_upload_policy_test.sh +++ b/test/works_drive_docker_image_upload_policy_test.sh @@ -14,19 +14,19 @@ fail() { [[ -f "$script" ]] || fail "WORKS Drive Docker image upload script must exist." [[ -f "$doc" ]] || fail "WORKS Drive Docker image archive design document must exist." -grep -Fq 'WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR:-docker-build-image' "$script" \ - || fail "script must default WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR to docker-build-image." +grep -Fq 'WORKS_DRIVE_DOCKER_IMAGE_DIR:-${WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR:-baron-sso}' "$script" \ + || fail "script must default WORKS_DRIVE_DOCKER_IMAGE_DIR to baron-sso with legacy fallback." grep -Fq 'docker commit' "$script" \ || fail "script must support committing a local container before image export." grep -Fq 'docker save' "$script" \ || fail "script must use docker save for CLI-compatible image artifacts." grep -Fq 'zstd' "$script" \ || fail "script must compress Docker image archives with zstd." -grep -Fq 'manifest.json' "$script" \ - || fail "script must write a manifest.json next to the image archive." -grep -Fq 'image.tar.zst.sha256' "$script" \ +grep -Fq 'manifest.${image_tag}.json' "$script" \ + || fail "script must write a versioned manifest next to the image archive." +grep -Fq '${image_name}.${image_tag}.sha256' "$script" \ || fail "script must write a checksum file for the compressed image archive." -grep -Fq 'docker-build-image/baron_sso/backend/v1.2606.ab12' "$doc" \ +grep -Fq 'baron-sso/v1.2606.ab12/backend.v1.2606.ab12.tar.zst' "$doc" \ || fail "document must describe the expected WORKS Drive folder layout." grep -Fq 'debian:trixie-slim' "$doc" \ || fail "document must use debian:trixie-slim for smoke image examples." @@ -124,24 +124,12 @@ case "$last_arg" in printf '{"files":[]}' ;; https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/root-folder/createfolder) - printf '{"fileId":"docker-build-image-id","fileName":"docker-build-image","fileType":"FOLDER"}' - ;; - https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/docker-build-image-id/children) - printf '{"files":[]}' - ;; - https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/docker-build-image-id/createfolder) - printf '{"fileId":"baron-sso-id","fileName":"baron_sso","fileType":"FOLDER"}' + printf '{"fileId":"baron-sso-id","fileName":"baron-sso","fileType":"FOLDER"}' ;; https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/baron-sso-id/children) printf '{"files":[]}' ;; https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/baron-sso-id/createfolder) - printf '{"fileId":"backend-id","fileName":"backend","fileType":"FOLDER"}' - ;; - https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/backend-id/children) - printf '{"files":[]}' - ;; - https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/backend-id/createfolder) printf '{"fileId":"tag-id","fileName":"v1.2606.ab12","fileType":"FOLDER"}' ;; https://www.worksapis.com/v1.0/sharedrives/shared-drive-1/files/tag-id) @@ -175,41 +163,41 @@ WORKS_DOCKER_COMMIT_CONTAINER="baron_backend" \ DOCKER_IMAGE_REF="registry.example/baron_sso/backend:v1.2606.ab12" \ "$script" >"$tmp_dir/upload.out" -artifact_dir="$archive_dir/baron_sso/backend/v1.2606.ab12" -[[ -f "$artifact_dir/image.tar.zst" ]] || fail "script must create image.tar.zst." -[[ -f "$artifact_dir/image.tar.zst.sha256" ]] || fail "script must create image.tar.zst.sha256." -[[ -f "$artifact_dir/manifest.json" ]] || fail "script must create manifest.json." +artifact_dir="$archive_dir/baron-sso/v1.2606.ab12" +[[ -f "$artifact_dir/backend.v1.2606.ab12.tar.zst" ]] || fail "script must create backend.v1.2606.ab12.tar.zst." +[[ -f "$artifact_dir/backend.v1.2606.ab12.sha256" ]] || fail "script must create backend.v1.2606.ab12.sha256." +[[ -f "$artifact_dir/manifest.v1.2606.ab12.json" ]] || fail "script must create manifest.v1.2606.ab12.json." jq -e \ '.schema_version == 1 and .format == "docker-save-zstd" and .image_ref == "registry.example/baron_sso/backend:v1.2606.ab12" and .repository == "baron_sso/backend" - and .tag == "v1.2606.ab12" - and .remote_path == "docker-build-image/baron_sso/backend/v1.2606.ab12" - and .archive.file_name == "image.tar.zst" - and (.archive.sha256 | type == "string")' \ - "$artifact_dir/manifest.json" >/dev/null || fail "manifest must describe the image archive and remote path." + and .release_repository == "baron-sso" + and .image_name == "backend" + and .tag == "v1.2606.ab12" + and .remote_path == "baron-sso/v1.2606.ab12" + and .archive.file_name == "backend.v1.2606.ab12.tar.zst" + and (.archive.sha256 | type == "string") + and .images.backend.archive.file_name == "backend.v1.2606.ab12.tar.zst" + and .images.backend.archive.sha256 == .archive.sha256' \ + "$artifact_dir/manifest.v1.2606.ab12.json" >/dev/null || fail "manifest must describe the image archive and remote path." grep -Fq "docker commit baron_backend registry.example/baron_sso/backend:v1.2606.ab12" "$docker_log" \ || fail "script must commit the requested container into the requested image ref." grep -Fq "docker save -o" "$docker_log" \ || fail "script must save the requested image." grep -Fq "sharedrives/shared-drive-1/files/root-folder/createfolder" "$curl_log" \ - || fail "script must create the top-level docker-build-image folder when needed." -grep -Fq "docker-build-image" "$curl_log" \ - || fail "script must use WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR in folder creation." -grep -Fq "baron_sso" "$curl_log" \ + || fail "script must create the top-level archive folder when needed." +grep -Fq "baron-sso" "$curl_log" \ || fail "script must create repository namespace folder." -grep -Fq "backend" "$curl_log" \ - || fail "script must create image repository folder." grep -Fq "v1.2606.ab12" "$curl_log" \ || fail "script must create tag folder." -grep -Fq "image.tar.zst" "$curl_log" \ +grep -Fq "backend.v1.2606.ab12.tar.zst" "$curl_log" \ || fail "script must upload the compressed image archive." -grep -Fq "image.tar.zst.sha256" "$curl_log" \ +grep -Fq "backend.v1.2606.ab12.sha256" "$curl_log" \ || fail "script must upload the checksum file." -grep -Fq "manifest.json" "$curl_log" \ +grep -Fq "manifest.v1.2606.ab12.json" "$curl_log" \ || fail "script must upload the manifest file." report_file="$artifact_dir/works-upload.json"