forked from baron/baron-sso
production 푸시 초안
This commit is contained in:
243
scripts/backup/refresh_works_drive_token.sh
Executable file
243
scripts/backup/refresh_works_drive_token.sh
Executable file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$script_dir/lib/common.sh"
|
||||
|
||||
repo_root="$(backup_repo_root)"
|
||||
env_file="${WORKS_DRIVE_ENV_FILE:-$repo_root/.env}"
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "$env_file"
|
||||
set +a
|
||||
fi
|
||||
|
||||
token_grant="${WORKS_DRIVE_TOKEN_GRANT:-refresh-token}"
|
||||
token_url="${WORKS_ADMIN_OAUTH_TOKEN_URL:-https://auth.worksmobile.com/oauth2/v2.0/token}"
|
||||
authorize_url="${WORKS_DRIVE_OAUTH_AUTHORIZE_URL:-https://auth.worksmobile.com/oauth2/v2.0/authorize}"
|
||||
client_id="${WORKS_DRIVE_OAUTH_CLIENT_ID:-}"
|
||||
client_secret="${WORKS_DRIVE_OAUTH_CLIENT_SECRET:-}"
|
||||
redirect_uri="${WORKS_DRIVE_OAUTH_REDIRECT_URI:-}"
|
||||
scope="${WORKS_DRIVE_OAUTH_SCOPE:-file}"
|
||||
curl_bin="${WORKS_DRIVE_CURL_BIN:-curl}"
|
||||
set_auth_mode="${WORKS_DRIVE_TOKEN_SET_AUTH_MODE:-true}"
|
||||
|
||||
backup_require_command jq
|
||||
|
||||
urlencode() {
|
||||
jq -nr --arg value "$1" '$value|@uri'
|
||||
}
|
||||
|
||||
redact_for_log() {
|
||||
sed -E 's/("(access_token|refresh_token|client_secret)"[[:space:]]*:[[:space:]]*)"[^"]*"/\1"REDACTED"/Ig'
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
require_oauth_client() {
|
||||
[[ -n "$client_id" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_ID is required."
|
||||
[[ -n "$client_secret" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_SECRET is required."
|
||||
}
|
||||
|
||||
extract_code_from_callback_url() {
|
||||
local raw_url="$1"
|
||||
local query
|
||||
local param
|
||||
local code=""
|
||||
local decoded
|
||||
|
||||
query="${raw_url#*\?}"
|
||||
query="${query%%#*}"
|
||||
|
||||
IFS='&' read -r -a params <<<"$query"
|
||||
for param in "${params[@]}"; do
|
||||
if [[ "$param" == code=* ]]; then
|
||||
code="${param#code=}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
decoded="${code//+/ }"
|
||||
printf '%b' "${decoded//%/\\x}"
|
||||
}
|
||||
|
||||
write_env_value() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
local tmp_file
|
||||
local env_uid=""
|
||||
local env_gid=""
|
||||
local env_mode=""
|
||||
|
||||
[[ -n "$key" ]] || backup_die "env key is required."
|
||||
[[ "$value" != *$'\n'* ]] || backup_die "env value for $key must not contain a newline."
|
||||
|
||||
mkdir -p "$(dirname "$env_file")"
|
||||
tmp_file="$(mktemp "$env_file.tmp.XXXXXX")"
|
||||
|
||||
if [[ -f "$env_file" ]]; then
|
||||
env_uid="$(stat -c '%u' "$env_file")"
|
||||
env_gid="$(stat -c '%g' "$env_file")"
|
||||
env_mode="$(stat -c '%a' "$env_file")"
|
||||
awk -v key="$key" -v value="$value" '
|
||||
BEGIN { written = 0 }
|
||||
$0 ~ "^[[:space:]]*" key "=" {
|
||||
print key "=" value
|
||||
written = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!written) {
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "$env_file" >"$tmp_file"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$value" >"$tmp_file"
|
||||
fi
|
||||
|
||||
if [[ -n "$env_mode" ]]; then
|
||||
chmod "$env_mode" "$tmp_file"
|
||||
else
|
||||
chmod 600 "$tmp_file"
|
||||
fi
|
||||
|
||||
if [[ -n "$env_uid" && -n "$env_gid" ]]; then
|
||||
chown "$env_uid:$env_gid" "$tmp_file" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
mv "$tmp_file" "$env_file"
|
||||
}
|
||||
|
||||
print_authorize_url() {
|
||||
[[ -n "$client_id" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_ID is required."
|
||||
[[ -n "$redirect_uri" ]] || backup_die "WORKS_DRIVE_OAUTH_REDIRECT_URI is required."
|
||||
|
||||
printf '%s?client_id=%s&redirect_uri=%s&scope=%s&response_type=code&state=%s\n' \
|
||||
"$authorize_url" \
|
||||
"$(urlencode "$client_id")" \
|
||||
"$(urlencode "$redirect_uri")" \
|
||||
"$(urlencode "$scope")" \
|
||||
"$(urlencode "${WORKS_DRIVE_OAUTH_STATE:-baron-sso-backup}")"
|
||||
}
|
||||
|
||||
request_refresh_token_grant() {
|
||||
require_oauth_client
|
||||
|
||||
local refresh_token="${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}"
|
||||
local response
|
||||
local response_body
|
||||
local http_status
|
||||
|
||||
[[ -n "$refresh_token" ]] || backup_die "WORKS_DRIVE_OAUTH_REFRESH_TOKEN is required for refresh-token grant."
|
||||
backup_require_command "$curl_bin"
|
||||
|
||||
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
--data-urlencode "grant_type=refresh_token" \
|
||||
--data-urlencode "refresh_token=$refresh_token" \
|
||||
--data-urlencode "client_id=$client_id" \
|
||||
--data-urlencode "client_secret=$client_secret" \
|
||||
"$token_url")"
|
||||
split_curl_response "$response" response_body http_status
|
||||
|
||||
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
|
||||
backup_die "WORKS refresh token request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$response_body"
|
||||
}
|
||||
|
||||
request_authorization_code_grant() {
|
||||
require_oauth_client
|
||||
|
||||
local code="${WORKS_DRIVE_AUTH_CODE:-}"
|
||||
local response
|
||||
local response_body
|
||||
local http_status
|
||||
|
||||
if [[ -z "$code" && -n "${WORKS_DRIVE_AUTH_CALLBACK_URL:-}" ]]; then
|
||||
code="$(extract_code_from_callback_url "$WORKS_DRIVE_AUTH_CALLBACK_URL")"
|
||||
fi
|
||||
|
||||
[[ -n "$code" ]] || backup_die "WORKS_DRIVE_AUTH_CODE or WORKS_DRIVE_AUTH_CALLBACK_URL is required for authorization-code grant."
|
||||
[[ -n "$redirect_uri" ]] || backup_die "WORKS_DRIVE_OAUTH_REDIRECT_URI is required for authorization-code grant."
|
||||
backup_require_command "$curl_bin"
|
||||
|
||||
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
--data-urlencode "grant_type=authorization_code" \
|
||||
--data-urlencode "code=$code" \
|
||||
--data-urlencode "client_id=$client_id" \
|
||||
--data-urlencode "client_secret=$client_secret" \
|
||||
--data-urlencode "redirect_uri=$redirect_uri" \
|
||||
"$token_url")"
|
||||
split_curl_response "$response" response_body http_status
|
||||
|
||||
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
|
||||
backup_die "WORKS authorization code token request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$response_body"
|
||||
}
|
||||
|
||||
persist_token_response() {
|
||||
local response_body="$1"
|
||||
local refresh_token
|
||||
local expires_in
|
||||
|
||||
expires_in="$(jq -r '.expires_in // empty' <<<"$response_body")"
|
||||
refresh_token="$(jq -r '.refresh_token // empty' <<<"$response_body")"
|
||||
|
||||
if [[ -n "$refresh_token" ]]; then
|
||||
write_env_value WORKS_DRIVE_OAUTH_REFRESH_TOKEN "$refresh_token"
|
||||
backup_log "WORKS Drive refresh token updated: $env_file"
|
||||
else
|
||||
backup_log "WORKS token refresh succeeded without a rotated refresh token."
|
||||
fi
|
||||
|
||||
if [[ "$set_auth_mode" == "true" ]]; then
|
||||
write_env_value WORKS_DRIVE_AUTH_MODE refresh-token
|
||||
fi
|
||||
|
||||
if [[ -n "$expires_in" ]]; then
|
||||
backup_log "WORKS Drive access token issued. expires_in=$expires_in"
|
||||
else
|
||||
backup_log "WORKS Drive access token issued."
|
||||
fi
|
||||
}
|
||||
|
||||
case "$token_grant" in
|
||||
print-authorize-url)
|
||||
print_authorize_url
|
||||
;;
|
||||
refresh-token)
|
||||
persist_token_response "$(request_refresh_token_grant)"
|
||||
;;
|
||||
authorization-code)
|
||||
persist_token_response "$(request_authorization_code_grant)"
|
||||
;;
|
||||
*)
|
||||
backup_die "unknown WORKS_DRIVE_TOKEN_GRANT: $token_grant. Expected refresh-token, authorization-code, or print-authorize-url."
|
||||
;;
|
||||
esac
|
||||
@@ -17,6 +17,7 @@ if [[ -f "$repo_root/.env" ]]; then
|
||||
WORKS_DRIVE_ACCESS_TOKEN
|
||||
WORKS_DRIVE_ACCESS_TOKEN_FILE
|
||||
WORKS_DRIVE_ACCESS_TOKEN_CMD
|
||||
WORKS_DRIVE_AUTH_MODE
|
||||
WORKS_DRIVE_OAUTH_SCOPE
|
||||
WORKS_DRIVE_SPLIT_SIZE
|
||||
WORKS_DRIVE_MAX_SINGLE_FILE_BYTES
|
||||
@@ -43,7 +44,7 @@ if [[ -f "$repo_root/.env" ]]; then
|
||||
declare -A env_override_values=()
|
||||
env_override_set=()
|
||||
for env_key in "${env_override_keys[@]}"; do
|
||||
if [[ -v "$env_key" ]]; then
|
||||
if [[ -n "${!env_key:-}" ]]; then
|
||||
env_override_set+=("$env_key")
|
||||
env_override_values["$env_key"]="${!env_key}"
|
||||
fi
|
||||
@@ -69,6 +70,7 @@ WORKS_DRIVE_PARENT_FILE_ID="${WORKS_DRIVE_PARENT_FILE_ID:-${WORKS_DRIVE_SHAREDRI
|
||||
|
||||
dry_run="${WORKS_DRIVE_DRY_RUN:-false}"
|
||||
target="${WORKS_DRIVE_TARGET:-sharedrive}"
|
||||
auth_mode="${WORKS_DRIVE_AUTH_MODE:-auto}"
|
||||
api_base_url="${WORKS_ADMIN_API_BASE_URL:-https://www.worksapis.com}"
|
||||
curl_bin="${WORKS_DRIVE_CURL_BIN:-curl}"
|
||||
archive_dir="${WORKS_DRIVE_ARCHIVE_DIR:-/tmp/baron-sso-backup-upload}"
|
||||
@@ -81,6 +83,11 @@ upload_reports="${WORKS_DRIVE_UPLOAD_REPORTS:-true}"
|
||||
report_folder_name="${WORKS_DRIVE_REPORT_FOLDER_NAME:-reports}"
|
||||
report_dir="$backup_path/reports"
|
||||
|
||||
case "$auth_mode" in
|
||||
auto | service-account | refresh-token) ;;
|
||||
*) backup_die "unknown WORKS_DRIVE_AUTH_MODE: $auth_mode. Expected auto, service-account, or refresh-token." ;;
|
||||
esac
|
||||
|
||||
if [[ -f "$backup_path" ]]; then
|
||||
report_dir="$(dirname "$backup_path")"
|
||||
fi
|
||||
@@ -330,6 +337,13 @@ request_refresh_access_token() {
|
||||
jq -er '.access_token' <<<"$response_body"
|
||||
}
|
||||
|
||||
service_account_credentials_configured() {
|
||||
[[ -n "${WORKS_DRIVE_OAUTH_CLIENT_ID:-}" ]] \
|
||||
&& [[ -n "${WORKS_DRIVE_OAUTH_CLIENT_SECRET:-}" ]] \
|
||||
&& [[ -n "${WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT:-}" ]] \
|
||||
&& { [[ -n "${WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY:-}" ]] || [[ -n "${WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY_FILE:-}" ]]; }
|
||||
}
|
||||
|
||||
resolve_access_token() {
|
||||
if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN:-}" ]]; then
|
||||
printf '%s\n' "$WORKS_DRIVE_ACCESS_TOKEN"
|
||||
@@ -347,12 +361,27 @@ resolve_access_token() {
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}" ]]; then
|
||||
request_refresh_access_token
|
||||
return
|
||||
fi
|
||||
case "$auth_mode" in
|
||||
service-account)
|
||||
request_service_account_token
|
||||
;;
|
||||
refresh-token)
|
||||
request_refresh_access_token
|
||||
;;
|
||||
auto)
|
||||
if service_account_credentials_configured; then
|
||||
request_service_account_token
|
||||
return
|
||||
fi
|
||||
|
||||
request_service_account_token
|
||||
if [[ -n "${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}" ]]; then
|
||||
request_refresh_access_token
|
||||
return
|
||||
fi
|
||||
|
||||
request_service_account_token
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
package_backup_path() {
|
||||
|
||||
162
scripts/deploy/build_image_deploy_bundle.sh
Executable file
162
scripts/deploy/build_image_deploy_bundle.sh
Executable file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_root="$(cd "$script_dir/../.." && pwd)"
|
||||
|
||||
die() {
|
||||
printf 'ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_env() {
|
||||
local key="$1"
|
||||
[[ -n "${!key:-}" ]] || die "Missing required env: $key"
|
||||
}
|
||||
|
||||
host_from_url() {
|
||||
local value="$1"
|
||||
value="${value#https://}"
|
||||
value="${value#http://}"
|
||||
printf '%s' "${value%%/*}"
|
||||
}
|
||||
|
||||
require_env IMAGE_TAG
|
||||
require_env IMAGE_DEPLOY_ENV
|
||||
require_env IMAGE_DEPLOY_PORT_PREFIX
|
||||
require_env IMAGE_DEPLOY_PUBLIC_URL
|
||||
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)"
|
||||
fi
|
||||
|
||||
case "$IMAGE_DEPLOY_ENV" in
|
||||
stage | staging)
|
||||
app_env="stage"
|
||||
default_instance_name="stage"
|
||||
;;
|
||||
production | prod)
|
||||
app_env="production"
|
||||
default_instance_name="prod"
|
||||
;;
|
||||
*)
|
||||
die "IMAGE_DEPLOY_ENV must be stage or production"
|
||||
;;
|
||||
esac
|
||||
|
||||
instance_name="${IMAGE_DEPLOY_INSTANCE_NAME:-$default_instance_name}"
|
||||
bundle_dir="${IMAGE_DEPLOY_BUNDLE_DIR:-$PWD/${instance_name}-image-deploy-bundle}"
|
||||
bundle_file="${IMAGE_DEPLOY_BUNDLE_FILE:-$PWD/${instance_name}-image-deploy-bundle.tgz}"
|
||||
compose_template="${IMAGE_DEPLOY_COMPOSE_TEMPLATE:-$repo_root/deploy/templates/docker-compose.images.yaml}"
|
||||
|
||||
rm -rf "$bundle_dir"
|
||||
TARGET_DIR="$bundle_dir" bash "$repo_root/deploy/create-instance.sh" "$instance_name" "$IMAGE_DEPLOY_PORT_PREFIX"
|
||||
cp "$compose_template" "$bundle_dir/docker-compose.yml"
|
||||
|
||||
public_host="$(host_from_url "$IMAGE_DEPLOY_PUBLIC_URL")"
|
||||
admin_host="$(host_from_url "$ADMINFRONT_URL")"
|
||||
dev_host="$(host_from_url "$DEVFRONT_URL")"
|
||||
org_host="$(host_from_url "$ORGFRONT_URL")"
|
||||
|
||||
cat >"$bundle_dir/.env" <<EOF
|
||||
INSTANCE_NAME=${instance_name}
|
||||
COMPOSE_PROJECT_NAME=baron-sso-${instance_name}
|
||||
APP_ENV=${app_env}
|
||||
TZ=Asia/Seoul
|
||||
SOURCE_ROOT=.
|
||||
P=${IMAGE_DEPLOY_PORT_PREFIX}
|
||||
DB_PORT=${IMAGE_DEPLOY_DB_PORT}
|
||||
REDIS_PORT=${IMAGE_DEPLOY_REDIS_PORT}
|
||||
CLICKHOUSE_PORT_HTTP=${IMAGE_DEPLOY_CLICKHOUSE_PORT_HTTP}
|
||||
CLICKHOUSE_PORT_NATIVE=${IMAGE_DEPLOY_CLICKHOUSE_PORT_NATIVE}
|
||||
BACKEND_PORT=${IMAGE_DEPLOY_BACKEND_PORT}
|
||||
USERFRONT_PORT=${IMAGE_DEPLOY_FRONTEND_PORT}
|
||||
ADMINFRONT_PORT=${ADMINFRONT_PORT}
|
||||
DEVFRONT_PORT=${DEVFRONT_PORT}
|
||||
ORGFRONT_PORT=${ORGFRONT_PORT}
|
||||
OATHKEEPER_PROXY_PORT=${IMAGE_DEPLOY_OATHKEEPER_PROXY_PORT}
|
||||
DOMAIN_SUFFIX=${IMAGE_DEPLOY_DOMAIN_SUFFIX}
|
||||
USERFRONT_URL=${IMAGE_DEPLOY_PUBLIC_URL}
|
||||
ADMINFRONT_URL=${ADMINFRONT_URL}
|
||||
DEVFRONT_URL=${DEVFRONT_URL}
|
||||
ORGFRONT_URL=${ORGFRONT_URL}
|
||||
PUBLIC_HOST=${public_host}
|
||||
ADMINFRONT_HOST=${admin_host}
|
||||
DEVFRONT_HOST=${dev_host}
|
||||
ORGFRONT_HOST=${org_host}
|
||||
TRAEFIK_PUBLIC_NETWORK=traefik-public
|
||||
TRAEFIK_ENTRYPOINT=websecure
|
||||
TRAEFIK_CERT_RESOLVER=myresolver
|
||||
VITE_OIDC_AUTHORITY=${VITE_OIDC_AUTHORITY}
|
||||
ADMINFRONT_CALLBACK_URLS=${ADMINFRONT_CALLBACK_URLS}
|
||||
DEVFRONT_CALLBACK_URLS=${DEVFRONT_CALLBACK_URLS}
|
||||
ORGFRONT_CALLBACK_URLS=${ORGFRONT_CALLBACK_URLS}
|
||||
KRATOS_UI_URL=${IMAGE_DEPLOY_PUBLIC_URL}/auth
|
||||
KRATOS_BROWSER_URL=${IMAGE_DEPLOY_PUBLIC_URL}/auth
|
||||
KRATOS_ADMIN_URL=http://kratos:4434
|
||||
HYDRA_PUBLIC_URL=${IMAGE_DEPLOY_PUBLIC_URL}/oidc
|
||||
HYDRA_ADMIN_URL=http://hydra:4445
|
||||
HYDRA_LOGIN_URL=${IMAGE_DEPLOY_PUBLIC_URL}/login
|
||||
HYDRA_CONSENT_URL=${IMAGE_DEPLOY_PUBLIC_URL}/consent
|
||||
HYDRA_ERROR_URL=${IMAGE_DEPLOY_PUBLIC_URL}/error
|
||||
HYDRA_REFRESH_TOKEN_TTL=${HYDRA_REFRESH_TOKEN_TTL}
|
||||
OATHKEEPER_PUBLIC_URL=${IMAGE_DEPLOY_PUBLIC_URL}
|
||||
KETO_READ_URL=http://keto:4466
|
||||
KETO_WRITE_URL=http://keto:4467
|
||||
IDP_PROVIDER=ory
|
||||
DB_PASSWORD=${IMAGE_DEPLOY_DB_PASSWORD}
|
||||
ORY_POSTGRES_USER=${ORY_POSTGRES_USER}
|
||||
ORY_POSTGRES_PASSWORD=${IMAGE_DEPLOY_ORY_POSTGRES_PASSWORD}
|
||||
ORY_POSTGRES_DB=${ORY_POSTGRES_DB}
|
||||
KRATOS_DB=${KRATOS_DB}
|
||||
HYDRA_DB=${HYDRA_DB}
|
||||
KETO_DB=${KETO_DB}
|
||||
KRATOS_VERSION=${KRATOS_VERSION}
|
||||
HYDRA_VERSION=${HYDRA_VERSION}
|
||||
KETO_VERSION=${KETO_VERSION}
|
||||
OATHKEEPER_VERSION=${OATHKEEPER_VERSION}
|
||||
ORY_POSTGRES_TAG=${ORY_POSTGRES_TAG}
|
||||
OATHKEEPER_UID=${OATHKEEPER_UID}
|
||||
OATHKEEPER_GID=${OATHKEEPER_GID}
|
||||
OATHKEEPER_INTROSPECT_CLIENT_ID=${OATHKEEPER_INTROSPECT_CLIENT_ID}
|
||||
OATHKEEPER_INTROSPECT_CLIENT_SECRET=${IMAGE_DEPLOY_OATHKEEPER_INTROSPECT_CLIENT_SECRET}
|
||||
CLICKHOUSE_PASSWORD=${IMAGE_DEPLOY_CLICKHOUSE_PASSWORD}
|
||||
REDIS_ADDR=redis:6379
|
||||
COOKIE_SECRET=${IMAGE_DEPLOY_COOKIE_SECRET}
|
||||
JWT_SECRET=${IMAGE_DEPLOY_JWT_SECRET}
|
||||
CSRF_COOKIE_SECRET=${IMAGE_DEPLOY_CSRF_COOKIE_SECRET}
|
||||
ADMIN_EMAIL=${ADMIN_EMAIL}
|
||||
ADMIN_PASSWORD=${IMAGE_DEPLOY_ADMIN_PASSWORD}
|
||||
IMAGE_TAG=${IMAGE_TAG}
|
||||
BACKEND_IMAGE_NAME=${BACKEND_IMAGE_NAME}
|
||||
USERFRONT_IMAGE_NAME=${USERFRONT_IMAGE_NAME}
|
||||
ADMINFRONT_IMAGE_NAME=${ADMINFRONT_IMAGE_NAME}
|
||||
DEVFRONT_IMAGE_NAME=${DEVFRONT_IMAGE_NAME}
|
||||
ORGFRONT_IMAGE_NAME=${ORGFRONT_IMAGE_NAME}
|
||||
EOF
|
||||
|
||||
chmod 600 "$bundle_dir/.env"
|
||||
|
||||
required_dotenv_keys="
|
||||
APP_ENV IMAGE_TAG BACKEND_IMAGE_NAME USERFRONT_IMAGE_NAME ADMINFRONT_IMAGE_NAME DEVFRONT_IMAGE_NAME ORGFRONT_IMAGE_NAME
|
||||
USERFRONT_URL PUBLIC_HOST HYDRA_PUBLIC_URL VITE_OIDC_AUTHORITY TRAEFIK_PUBLIC_NETWORK
|
||||
DB_PASSWORD ORY_POSTGRES_PASSWORD COOKIE_SECRET JWT_SECRET CSRF_COOKIE_SECRET
|
||||
"
|
||||
for key in $required_dotenv_keys; do
|
||||
if ! grep -Eq "^${key}=.+" "$bundle_dir/.env"; then
|
||||
die "Missing required bundle .env value: $key"
|
||||
fi
|
||||
done
|
||||
|
||||
ORY_CONFIG_ENV_FILES="$bundle_dir/.env" \
|
||||
ORY_CONFIG_TEMPLATE_ROOT="$bundle_dir/ory/templates" \
|
||||
ORY_CONFIG_OUTPUT_DIR="$bundle_dir/config/.generated/ory" \
|
||||
bash "$repo_root/scripts/render_ory_config.sh"
|
||||
|
||||
tar -C "$bundle_dir" -czf "$bundle_file" .
|
||||
printf '%s\n' "$bundle_file"
|
||||
39
scripts/deploy/upload_and_run_image_deploy.sh
Executable file
39
scripts/deploy/upload_and_run_image_deploy.sh
Executable file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
die() {
|
||||
printf 'ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_env() {
|
||||
local key="$1"
|
||||
[[ -n "${!key:-}" ]] || die "Missing required env: $key"
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
[[ -f "$IMAGE_DEPLOY_BUNDLE_FILE" ]] || die "bundle file not found: $IMAGE_DEPLOY_BUNDLE_FILE"
|
||||
|
||||
remote_bundle="/tmp/baron-sso-image-deploy-$(date -u '+%Y%m%d%H%M%S').tgz"
|
||||
|
||||
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}" \
|
||||
"set -euo pipefail; \
|
||||
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; \
|
||||
docker compose --env-file .env -f docker-compose.yml up -d --remove-orphans; \
|
||||
docker compose --env-file .env -f docker-compose.yml ps"
|
||||
632
scripts/docker-image/upload_works_drive.sh
Executable file
632
scripts/docker-image/upload_works_drive.sh
Executable file
@@ -0,0 +1,632 @@
|
||||
#!/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"
|
||||
|
||||
if [[ -f "$repo_root/.env" ]]; then
|
||||
env_override_keys=(
|
||||
DOCKER_IMAGE_REF
|
||||
IMAGE_REF
|
||||
WORKS_DOCKER_COMMIT_CONTAINER
|
||||
DOCKER_COMMIT_CONTAINER
|
||||
WORKS_DOCKER_IMAGE_ARCHIVE_DIR
|
||||
WORKS_SHAREDRIVE_DOCKER_IMAGE_DIR
|
||||
WORKS_DRIVE_TARGET
|
||||
WORKS_DRIVE_SHARED_DRIVE_ID
|
||||
WORKS_DRIVE_PARENT_FILE_ID
|
||||
WORKS_DRIVE_USER_ID
|
||||
WORKS_DRIVE_GROUP_ID
|
||||
WORKS_DRIVE_SHARED_FOLDER_ID
|
||||
WORKS_DRIVE_ACCESS_TOKEN
|
||||
WORKS_DRIVE_ACCESS_TOKEN_FILE
|
||||
WORKS_DRIVE_ACCESS_TOKEN_CMD
|
||||
WORKS_DRIVE_OAUTH_SCOPE
|
||||
WORKS_DRIVE_OVERWRITE
|
||||
WORKS_DRIVE_DRY_RUN
|
||||
WORKS_DRIVE_CURL_BIN
|
||||
WORKS_DRIVE_SHAREDRIVE_ID
|
||||
WORKS_DRIVE_OAUTH_CLIENT_ID
|
||||
WORKS_DRIVE_OAUTH_CLIENT_SECRET
|
||||
WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT
|
||||
WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY
|
||||
WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY_FILE
|
||||
WORKS_DRIVE_OAUTH_REFRESH_TOKEN
|
||||
WORKS_SHAREDRIVE_ID
|
||||
WORKS_ADMIN_API_BASE_URL
|
||||
WORKS_ADMIN_OAUTH_TOKEN_URL
|
||||
)
|
||||
declare -A env_override_values=()
|
||||
env_override_set=()
|
||||
for env_key in "${env_override_keys[@]}"; do
|
||||
if [[ -v "$env_key" ]]; then
|
||||
env_override_set+=("$env_key")
|
||||
env_override_values["$env_key"]="${!env_key}"
|
||||
fi
|
||||
done
|
||||
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "$repo_root/.env"
|
||||
set +a
|
||||
|
||||
for env_key in "${env_override_set[@]}"; do
|
||||
printf -v "$env_key" '%s' "${env_override_values[$env_key]}"
|
||||
export "$env_key"
|
||||
done
|
||||
fi
|
||||
|
||||
backup_require_command docker
|
||||
backup_require_command jq
|
||||
backup_require_command sha256sum
|
||||
backup_require_command stat
|
||||
backup_require_command zstd
|
||||
|
||||
image_ref="${DOCKER_IMAGE_REF:-${IMAGE_REF:-}}"
|
||||
[[ -n "$image_ref" ]] || backup_die "DOCKER_IMAGE_REF is required. Example: DOCKER_IMAGE_REF=registry.example/baron_sso/backend:v1.2606.ab12"
|
||||
|
||||
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}"
|
||||
dry_run="${WORKS_DRIVE_DRY_RUN:-false}"
|
||||
target="${WORKS_DRIVE_TARGET:-sharedrive}"
|
||||
api_base_url="${WORKS_ADMIN_API_BASE_URL:-https://www.worksapis.com}"
|
||||
curl_bin="${WORKS_DRIVE_CURL_BIN:-curl}"
|
||||
overwrite="${WORKS_DRIVE_OVERWRITE:-true}"
|
||||
upload_scope="${WORKS_DRIVE_OAUTH_SCOPE:-file}"
|
||||
|
||||
WORKS_DRIVE_SHARED_DRIVE_ID="${WORKS_DRIVE_SHARED_DRIVE_ID:-${WORKS_DRIVE_SHAREDRIVE_ID:-${WORKS_SHAREDRIVE_ID:-}}}"
|
||||
|
||||
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_target_upload_endpoint() {
|
||||
local parent_file_id="${1:-${WORKS_DRIVE_PARENT_FILE_ID:-}}"
|
||||
local encoded_parent=""
|
||||
|
||||
if [[ -n "$parent_file_id" ]]; then
|
||||
encoded_parent="$(urlencode_path "$parent_file_id")"
|
||||
fi
|
||||
|
||||
case "$target" in
|
||||
sharedrive)
|
||||
[[ -n "${WORKS_DRIVE_SHARED_DRIVE_ID:-}" ]] || backup_die "WORKS_DRIVE_SHARED_DRIVE_ID is required when WORKS_DRIVE_TARGET=sharedrive."
|
||||
local shared_drive_id
|
||||
shared_drive_id="$(urlencode_path "$WORKS_DRIVE_SHARED_DRIVE_ID")"
|
||||
if [[ -n "$encoded_parent" ]]; then
|
||||
printf '%s/v1.0/sharedrives/%s/files/%s\n' "$api_base_url" "$shared_drive_id" "$encoded_parent"
|
||||
else
|
||||
printf '%s/v1.0/sharedrives/%s/files\n' "$api_base_url" "$shared_drive_id"
|
||||
fi
|
||||
;;
|
||||
mydrive)
|
||||
local user_id="${WORKS_DRIVE_USER_ID:-me}"
|
||||
user_id="$(urlencode_path "$user_id")"
|
||||
if [[ -n "$encoded_parent" ]]; then
|
||||
printf '%s/v1.0/users/%s/drive/files/%s\n' "$api_base_url" "$user_id" "$encoded_parent"
|
||||
else
|
||||
printf '%s/v1.0/users/%s/drive/files\n' "$api_base_url" "$user_id"
|
||||
fi
|
||||
;;
|
||||
group)
|
||||
[[ -n "${WORKS_DRIVE_GROUP_ID:-}" ]] || backup_die "WORKS_DRIVE_GROUP_ID is required when WORKS_DRIVE_TARGET=group."
|
||||
local group_id
|
||||
group_id="$(urlencode_path "$WORKS_DRIVE_GROUP_ID")"
|
||||
if [[ -n "$encoded_parent" ]]; then
|
||||
printf '%s/v1.0/groups/%s/folder/files/%s\n' "$api_base_url" "$group_id" "$encoded_parent"
|
||||
else
|
||||
printf '%s/v1.0/groups/%s/folder/files\n' "$api_base_url" "$group_id"
|
||||
fi
|
||||
;;
|
||||
sharedfolder)
|
||||
[[ -n "${WORKS_DRIVE_SHARED_FOLDER_ID:-}" ]] || backup_die "WORKS_DRIVE_SHARED_FOLDER_ID is required when WORKS_DRIVE_TARGET=sharedfolder."
|
||||
local user_id="${WORKS_DRIVE_USER_ID:-me}"
|
||||
local shared_folder_id
|
||||
user_id="$(urlencode_path "$user_id")"
|
||||
shared_folder_id="$(urlencode_path "$WORKS_DRIVE_SHARED_FOLDER_ID")"
|
||||
if [[ -n "$encoded_parent" ]]; then
|
||||
printf '%s/v1.0/users/%s/drive/sharedfolders/%s/files/%s\n' "$api_base_url" "$user_id" "$shared_folder_id" "$encoded_parent"
|
||||
else
|
||||
printf '%s/v1.0/users/%s/drive/sharedfolders/%s/files\n' "$api_base_url" "$user_id" "$shared_folder_id"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
backup_die "unknown WORKS_DRIVE_TARGET: $target"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_target_children_endpoint() {
|
||||
local parent_file_id="${1:-${WORKS_DRIVE_PARENT_FILE_ID:-}}"
|
||||
printf '%s/children\n' "$(resolve_target_upload_endpoint "$parent_file_id")"
|
||||
}
|
||||
|
||||
resolve_target_create_folder_endpoint() {
|
||||
local parent_file_id="${1:-${WORKS_DRIVE_PARENT_FILE_ID:-}}"
|
||||
printf '%s/createfolder\n' "$(resolve_target_upload_endpoint "$parent_file_id")"
|
||||
}
|
||||
|
||||
base64url() {
|
||||
openssl base64 -A | tr '+/' '-_' | tr -d '='
|
||||
}
|
||||
|
||||
build_jwt_assertion() {
|
||||
backup_require_command openssl
|
||||
|
||||
local client_id="${WORKS_DRIVE_OAUTH_CLIENT_ID:-}"
|
||||
local service_account="${WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT:-}"
|
||||
local private_key="${WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY:-}"
|
||||
local private_key_file="${WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY_FILE:-}"
|
||||
local key_file=""
|
||||
local temp_key_file=""
|
||||
local now
|
||||
local exp
|
||||
local header
|
||||
local payload
|
||||
local signing_input
|
||||
|
||||
[[ -n "$client_id" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_ID is required for service-account token mode."
|
||||
[[ -n "$service_account" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_SERVICE_ACCOUNT is required for service-account token mode."
|
||||
|
||||
if [[ -n "$private_key" ]]; then
|
||||
temp_key_file="$(mktemp /tmp/baron-sso-works-key.XXXXXX)"
|
||||
printf '%s\n' "$private_key" >"$temp_key_file"
|
||||
key_file="$temp_key_file"
|
||||
elif [[ -n "$private_key_file" ]]; then
|
||||
if [[ "$private_key_file" != /* ]]; then
|
||||
private_key_file="$repo_root/$private_key_file"
|
||||
fi
|
||||
backup_require_path "$private_key_file"
|
||||
key_file="$private_key_file"
|
||||
else
|
||||
backup_die "WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY or WORKS_DRIVE_OAUTH_CLIENT_PRIVATE_KEY_FILE is required for service-account token mode."
|
||||
fi
|
||||
|
||||
now="$(date +%s)"
|
||||
exp="$((now + 3600))"
|
||||
header="$(printf '{"alg":"RS256","typ":"JWT"}' | base64url)"
|
||||
payload="$(jq -cn \
|
||||
--arg iss "$client_id" \
|
||||
--arg sub "$service_account" \
|
||||
--argjson iat "$now" \
|
||||
--argjson exp "$exp" \
|
||||
'{iss:$iss, sub:$sub, iat:$iat, exp:$exp}' | base64url)"
|
||||
signing_input="${header}.${payload}"
|
||||
|
||||
printf '%s' "$signing_input" \
|
||||
| openssl dgst -sha256 -sign "$key_file" -binary \
|
||||
| base64url \
|
||||
| while IFS= read -r signature; do
|
||||
printf '%s.%s\n' "$signing_input" "$signature"
|
||||
done
|
||||
|
||||
if [[ -n "$temp_key_file" ]]; then
|
||||
rm -f "$temp_key_file"
|
||||
fi
|
||||
}
|
||||
|
||||
request_service_account_token() {
|
||||
local client_id="${WORKS_DRIVE_OAUTH_CLIENT_ID:-}"
|
||||
local client_secret="${WORKS_DRIVE_OAUTH_CLIENT_SECRET:-}"
|
||||
local token_url="${WORKS_ADMIN_OAUTH_TOKEN_URL:-https://auth.worksmobile.com/oauth2/v2.0/token}"
|
||||
local assertion
|
||||
local response
|
||||
local response_body
|
||||
local http_status
|
||||
|
||||
[[ -n "$client_secret" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_SECRET is required for service-account token mode."
|
||||
assertion="$(build_jwt_assertion)"
|
||||
|
||||
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
|
||||
--data-urlencode "assertion=$assertion" \
|
||||
--data-urlencode "client_id=$client_id" \
|
||||
--data-urlencode "client_secret=$client_secret" \
|
||||
--data-urlencode "scope=$upload_scope" \
|
||||
"$token_url")"
|
||||
split_curl_response "$response" response_body http_status
|
||||
|
||||
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
|
||||
backup_die "WORKS token request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
|
||||
fi
|
||||
|
||||
jq -er '.access_token' <<<"$response_body"
|
||||
}
|
||||
|
||||
request_refresh_access_token() {
|
||||
local client_id="${WORKS_DRIVE_OAUTH_CLIENT_ID:-}"
|
||||
local client_secret="${WORKS_DRIVE_OAUTH_CLIENT_SECRET:-}"
|
||||
local refresh_token="${WORKS_DRIVE_OAUTH_REFRESH_TOKEN:-}"
|
||||
local token_url="${WORKS_ADMIN_OAUTH_TOKEN_URL:-https://auth.worksmobile.com/oauth2/v2.0/token}"
|
||||
local response
|
||||
local response_body
|
||||
local http_status
|
||||
|
||||
[[ -n "$client_id" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_ID is required for refresh-token mode."
|
||||
[[ -n "$client_secret" ]] || backup_die "WORKS_DRIVE_OAUTH_CLIENT_SECRET is required for refresh-token mode."
|
||||
[[ -n "$refresh_token" ]] || backup_die "WORKS_DRIVE_OAUTH_REFRESH_TOKEN is required for refresh-token mode."
|
||||
|
||||
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
--data-urlencode "grant_type=refresh_token" \
|
||||
--data-urlencode "refresh_token=$refresh_token" \
|
||||
--data-urlencode "client_id=$client_id" \
|
||||
--data-urlencode "client_secret=$client_secret" \
|
||||
"$token_url")"
|
||||
split_curl_response "$response" response_body http_status
|
||||
|
||||
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
|
||||
backup_die "WORKS refresh token request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
|
||||
fi
|
||||
|
||||
jq -er '.access_token' <<<"$response_body"
|
||||
}
|
||||
|
||||
resolve_access_token() {
|
||||
if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN:-}" ]]; then
|
||||
printf '%s\n' "$WORKS_DRIVE_ACCESS_TOKEN"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "${WORKS_DRIVE_ACCESS_TOKEN_FILE:-}" ]]; then
|
||||
backup_require_path "$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
|
||||
request_refresh_access_token
|
||||
return
|
||||
fi
|
||||
|
||||
request_service_account_token
|
||||
}
|
||||
|
||||
list_child_folders() {
|
||||
local access_token="$1"
|
||||
local endpoint="$2"
|
||||
local response
|
||||
local response_body
|
||||
local http_status
|
||||
|
||||
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X GET \
|
||||
-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"
|
||||
}
|
||||
|
||||
create_child_folder() {
|
||||
local access_token="$1"
|
||||
local endpoint="$2"
|
||||
local folder_name="$3"
|
||||
local payload
|
||||
local response
|
||||
local response_body
|
||||
local http_status
|
||||
|
||||
payload="$(jq -cn --arg fileName "$folder_name" '{fileName:$fileName}')"
|
||||
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
|
||||
-H "Authorization: Bearer $access_token" \
|
||||
-H "Content-Type: application/json; charset=UTF-8" \
|
||||
-d "$payload" \
|
||||
"$endpoint")"
|
||||
split_curl_response "$response" response_body http_status
|
||||
|
||||
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
|
||||
backup_die "WORKS folder create request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
|
||||
fi
|
||||
|
||||
jq -er '.fileId // .id' <<<"$response_body"
|
||||
}
|
||||
|
||||
ensure_child_folder() {
|
||||
local access_token="$1"
|
||||
local parent_file_id="$2"
|
||||
local folder_name="$3"
|
||||
local children_endpoint
|
||||
local create_folder_endpoint
|
||||
local children_json
|
||||
local folder_id
|
||||
|
||||
children_endpoint="$(resolve_target_children_endpoint "$parent_file_id")"
|
||||
create_folder_endpoint="$(resolve_target_create_folder_endpoint "$parent_file_id")"
|
||||
children_json="$(list_child_folders "$access_token" "$children_endpoint")"
|
||||
folder_id="$(jq -er --arg name "$folder_name" '
|
||||
[
|
||||
(.files // .children // .items // [])[]
|
||||
| select((.fileName // .name) == $name)
|
||||
| select(((.fileType // .type // "") | ascii_downcase) == "folder")
|
||||
| .fileId // .id
|
||||
][0] // empty
|
||||
' <<<"$children_json" 2>/dev/null || true)"
|
||||
|
||||
if [[ -n "$folder_id" ]]; then
|
||||
printf '%s\n' "$folder_id"
|
||||
return
|
||||
fi
|
||||
|
||||
create_child_folder "$access_token" "$create_folder_endpoint" "$folder_name"
|
||||
}
|
||||
|
||||
ensure_folder_path() {
|
||||
local access_token="$1"
|
||||
local path="$2"
|
||||
local parent_file_id="${WORKS_DRIVE_PARENT_FILE_ID:-}"
|
||||
local component
|
||||
|
||||
IFS='/' read -r -a components <<<"$path"
|
||||
for component in "${components[@]}"; do
|
||||
[[ -n "$component" ]] || continue
|
||||
parent_file_id="$(ensure_child_folder "$access_token" "$parent_file_id" "$component")"
|
||||
done
|
||||
|
||||
printf '%s\n' "$parent_file_id"
|
||||
}
|
||||
|
||||
create_upload_url() {
|
||||
local access_token="$1"
|
||||
local endpoint="$2"
|
||||
local file_path="$3"
|
||||
local file_name
|
||||
local file_size
|
||||
local payload
|
||||
local response
|
||||
local response_body
|
||||
local http_status
|
||||
|
||||
file_name="$(basename "$file_path")"
|
||||
file_size="$(stat -c '%s' "$file_path")"
|
||||
payload="$(jq -cn \
|
||||
--arg fileName "$file_name" \
|
||||
--argjson fileSize "$file_size" \
|
||||
--argjson overwrite "$overwrite" \
|
||||
'{fileName:$fileName, fileSize:$fileSize, overwrite:$overwrite}')"
|
||||
|
||||
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
|
||||
-H "Authorization: Bearer $access_token" \
|
||||
-H "Content-Type: application/json; charset=UTF-8" \
|
||||
-d "$payload" \
|
||||
"$endpoint")"
|
||||
split_curl_response "$response" response_body http_status
|
||||
|
||||
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
|
||||
backup_die "WORKS upload URL request failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
|
||||
fi
|
||||
|
||||
jq -er '.uploadUrl' <<<"$response_body"
|
||||
}
|
||||
|
||||
upload_file_to_url() {
|
||||
local access_token="$1"
|
||||
local upload_url="$2"
|
||||
local file_path="$3"
|
||||
local file_name
|
||||
local response
|
||||
local response_body
|
||||
local http_status
|
||||
|
||||
file_name="$(basename "$file_path")"
|
||||
response="$("$curl_bin" -sS -w $'\n%{http_code}' -X POST \
|
||||
-H "Authorization: Bearer $access_token" \
|
||||
-F "Filedata=@${file_path};filename=${file_name};type=application/octet-stream" \
|
||||
"$upload_url")"
|
||||
split_curl_response "$response" response_body http_status
|
||||
|
||||
if [[ "$http_status" -lt 200 || "$http_status" -ge 300 ]]; then
|
||||
backup_die "WORKS file upload failed (HTTP $http_status): $(printf '%s' "$response_body" | redact_for_log)"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$response_body"
|
||||
}
|
||||
|
||||
derive_repository_and_tag() {
|
||||
local ref="$1"
|
||||
local ref_without_digest="${ref%@*}"
|
||||
local last_slash_index=-1
|
||||
local last_colon_index=-1
|
||||
local i
|
||||
local char
|
||||
local repository_with_registry
|
||||
local first_component
|
||||
local rest
|
||||
|
||||
if [[ "$ref" == *@* ]]; then
|
||||
backup_die "digest image refs are not supported for WORKS image archive upload. Use a tagged image ref."
|
||||
fi
|
||||
|
||||
for ((i = 0; i < ${#ref_without_digest}; i += 1)); do
|
||||
char="${ref_without_digest:i:1}"
|
||||
[[ "$char" == "/" ]] && last_slash_index="$i"
|
||||
[[ "$char" == ":" ]] && last_colon_index="$i"
|
||||
done
|
||||
|
||||
if [[ "$last_colon_index" -le "$last_slash_index" ]]; then
|
||||
backup_die "DOCKER_IMAGE_REF must include an explicit tag: $ref"
|
||||
fi
|
||||
|
||||
repository_with_registry="${ref_without_digest:0:last_colon_index}"
|
||||
image_tag="${ref_without_digest:last_colon_index + 1}"
|
||||
|
||||
first_component="${repository_with_registry%%/*}"
|
||||
if [[ "$repository_with_registry" == */* ]]; then
|
||||
rest="${repository_with_registry#*/}"
|
||||
else
|
||||
rest="$repository_with_registry"
|
||||
fi
|
||||
|
||||
if [[ "$first_component" == *.* || "$first_component" == *:* || "$first_component" == "localhost" ]]; then
|
||||
image_repository="$rest"
|
||||
else
|
||||
image_repository="$repository_with_registry"
|
||||
fi
|
||||
|
||||
[[ -n "$image_repository" ]] || backup_die "image repository could not be derived from DOCKER_IMAGE_REF: $ref"
|
||||
[[ "$image_repository" =~ ^[A-Za-z0-9._/-]+$ ]] || backup_die "image repository contains unsupported characters: $image_repository"
|
||||
[[ "$image_tag" =~ ^[A-Za-z0-9._-]+$ ]] || backup_die "image tag contains unsupported characters: $image_tag"
|
||||
}
|
||||
|
||||
derive_repository_and_tag "$image_ref"
|
||||
|
||||
remote_path="${image_root_dir}/${image_repository}/${image_tag}"
|
||||
artifact_dir="${archive_root}/${image_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"
|
||||
upload_report_file="$artifact_dir/works-upload.json"
|
||||
|
||||
rm -f "$tar_file" "$archive_file" "$checksum_file" "$manifest_file" "$upload_report_file"
|
||||
|
||||
if [[ -n "$commit_container" ]]; then
|
||||
backup_log "Committing container $commit_container to $image_ref"
|
||||
docker commit "$commit_container" "$image_ref" >/dev/null
|
||||
fi
|
||||
|
||||
backup_log "Saving Docker image $image_ref"
|
||||
docker save -o "$tar_file" "$image_ref"
|
||||
|
||||
backup_log "Compressing image archive with zstd"
|
||||
zstd -f -19 -o "$archive_file" "$tar_file" >/dev/null
|
||||
rm -f "$tar_file"
|
||||
|
||||
archive_sha256="$(sha256sum "$archive_file" | awk '{print $1}')"
|
||||
printf '%s %s\n' "$archive_sha256" "$(basename "$archive_file")" >"$checksum_file"
|
||||
|
||||
image_id="$(docker image inspect "$image_ref" --format '{{.Id}}' 2>/dev/null || printf 'unknown')"
|
||||
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,
|
||||
image_ref: $imageRef,
|
||||
repository: $repository,
|
||||
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"
|
||||
|
||||
upload_files=("$archive_file" "$checksum_file" "$manifest_file")
|
||||
|
||||
if [[ "$dry_run" == "true" ]]; then
|
||||
jq -n \
|
||||
--arg createdAt "$(backup_utc_now)" \
|
||||
--arg status "planned" \
|
||||
--arg remotePath "$remote_path" \
|
||||
--arg artifactDir "$artifact_dir" \
|
||||
--argjson files "$(printf '%s\n' "${upload_files[@]}" | jq -R '{file_path:., file_name:(. | split("/")[-1]), status:"planned"}' | jq -s '.')" \
|
||||
'{
|
||||
created_at: $createdAt,
|
||||
status: $status,
|
||||
remote_path: $remotePath,
|
||||
artifact_dir: $artifactDir,
|
||||
files: $files
|
||||
}' >"$upload_report_file"
|
||||
backup_log "Dry run: packaged image artifacts at $artifact_dir"
|
||||
backup_log "Dry run: would upload to WORKS Drive path $remote_path"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
backup_require_command "$curl_bin"
|
||||
access_token="$(resolve_access_token)"
|
||||
backup_log "Resolving WORKS Drive folder path: $remote_path"
|
||||
target_folder_id="$(ensure_folder_path "$access_token" "$remote_path")"
|
||||
upload_endpoint="$(resolve_target_upload_endpoint "$target_folder_id")"
|
||||
|
||||
uploaded_items="[]"
|
||||
for file_path in "${upload_files[@]}"; do
|
||||
backup_require_path "$file_path"
|
||||
backup_log "Creating WORKS Drive upload URL for $(basename "$file_path")"
|
||||
upload_url="$(create_upload_url "$access_token" "$upload_endpoint" "$file_path")"
|
||||
backup_log "Uploading $(basename "$file_path") to WORKS Drive"
|
||||
upload_response="$(upload_file_to_url "$access_token" "$upload_url" "$file_path")"
|
||||
upload_response_json="$(jq -c '.' <<<"${upload_response:-{}}" 2>/dev/null || printf '{}')"
|
||||
uploaded_items="$(jq \
|
||||
--arg fileName "$(basename "$file_path")" \
|
||||
--arg filePath "$file_path" \
|
||||
--argjson fileSize "$(stat -c '%s' "$file_path")" \
|
||||
--arg status "uploaded" \
|
||||
--arg response "$upload_response_json" \
|
||||
'. + [{file_name:$fileName, file_path:$filePath, file_size:$fileSize, status:$status, response:($response | fromjson? // {})}]' \
|
||||
<<<"$uploaded_items")"
|
||||
done
|
||||
|
||||
jq -n \
|
||||
--arg createdAt "$(backup_utc_now)" \
|
||||
--arg status "uploaded" \
|
||||
--arg remotePath "$remote_path" \
|
||||
--arg artifactDir "$artifact_dir" \
|
||||
--arg target "$target" \
|
||||
--arg folderId "$target_folder_id" \
|
||||
--argjson files "$uploaded_items" \
|
||||
'{
|
||||
created_at: $createdAt,
|
||||
status: $status,
|
||||
target: $target,
|
||||
remote_path: $remotePath,
|
||||
target_folder_id: $folderId,
|
||||
artifact_dir: $artifactDir,
|
||||
files: $files
|
||||
}' >"$upload_report_file"
|
||||
|
||||
backup_log "Upload complete: $upload_report_file"
|
||||
Reference in New Issue
Block a user