feat(deploy): add production deployment workflow and image deployment script
Deployment Config CI / Deployment Config (push) Successful in 20s
Deployment Config CI / Deployment Config (push) Successful in 20s
This commit is contained in:
@@ -72,4 +72,4 @@ jobs:
|
||||
docker image rm geo-rankly/tenant-api:ci-smoke >/dev/null 2>&1 || true
|
||||
|
||||
- name: Validate shell scripts
|
||||
run: bash -n deploy/scripts/package.sh deploy/scripts/load-and-start.sh deploy/scripts/gitea-registry-images.sh
|
||||
run: bash -n deploy/scripts/package.sh deploy/scripts/load-and-start.sh deploy/scripts/gitea-registry-images.sh deploy/scripts/deploy-images-to-k3s.sh
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
name: Deploy Production K3s
|
||||
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: "Service or group to deploy to production k3s"
|
||||
required: true
|
||||
default: "ops-web"
|
||||
type: choice
|
||||
options:
|
||||
- frontend
|
||||
- ops-web
|
||||
- browser-fetch
|
||||
- tenant-api
|
||||
- ops-api
|
||||
- worker-generate
|
||||
- kol-assist-worker
|
||||
- scheduler
|
||||
- web
|
||||
- backend
|
||||
- all-apps
|
||||
- migrate
|
||||
image_tag:
|
||||
description: "Image tag to deploy. Empty means current commit short SHA. Use latest only if you really want the moving tag."
|
||||
required: false
|
||||
default: ""
|
||||
run_migrations:
|
||||
description: "Run database migrations before app rollout"
|
||||
required: true
|
||||
default: "auto"
|
||||
type: choice
|
||||
options:
|
||||
- auto
|
||||
- yes
|
||||
- no
|
||||
host:
|
||||
description: "Production k3s SSH host"
|
||||
required: true
|
||||
default: "39.105.229.239"
|
||||
port:
|
||||
description: "Production k3s SSH port"
|
||||
required: true
|
||||
default: "22"
|
||||
user:
|
||||
description: "Production k3s SSH user"
|
||||
required: true
|
||||
default: "root"
|
||||
password_source:
|
||||
description: "Production SSH password source"
|
||||
required: true
|
||||
default: "repository_secret"
|
||||
type: choice
|
||||
options:
|
||||
- repository_secret
|
||||
- workflow_input
|
||||
ssh_password:
|
||||
description: "Production SSH password. Used only when password_source=workflow_input."
|
||||
required: false
|
||||
default: ""
|
||||
namespace:
|
||||
description: "Kubernetes namespace"
|
||||
required: true
|
||||
default: "geo-rankly"
|
||||
registry_host:
|
||||
description: "NAS Gitea Registry host, without scheme"
|
||||
required: true
|
||||
default: "192.168.100.19:13000"
|
||||
registry_owner:
|
||||
description: "Gitea package owner"
|
||||
required: true
|
||||
default: "root"
|
||||
registry_image_prefix:
|
||||
description: "Image package prefix. Images are <prefix>/<service>."
|
||||
required: true
|
||||
default: "geo-rankly"
|
||||
registry_username:
|
||||
description: "Gitea Registry username"
|
||||
required: true
|
||||
default: "root"
|
||||
registry_password_source:
|
||||
description: "Gitea Registry password source"
|
||||
required: true
|
||||
default: "repository_secret"
|
||||
type: choice
|
||||
options:
|
||||
- repository_secret
|
||||
- workflow_input
|
||||
registry_password:
|
||||
description: "Gitea Registry password/token. Used only when registry_password_source=workflow_input."
|
||||
required: false
|
||||
default: ""
|
||||
registry_plain_http:
|
||||
description: "Registry uses plain HTTP and must be configured as a Docker insecure registry on the runner."
|
||||
required: true
|
||||
default: "true"
|
||||
type: choice
|
||||
options:
|
||||
- "true"
|
||||
- "false"
|
||||
wait_timeout:
|
||||
description: "Kubernetes rollout/migration wait timeout"
|
||||
required: false
|
||||
default: "10m"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-production-k3s:
|
||||
name: Deploy selected target to production k3s
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
env:
|
||||
PROD_HOST: ${{ inputs.host }}
|
||||
PROD_PORT: ${{ inputs.port }}
|
||||
PROD_USER: ${{ inputs.user }}
|
||||
PROD_SSH_PASSWORD_SOURCE: ${{ inputs.password_source }}
|
||||
PROD_SSH_PASSWORD_INPUT: ${{ inputs.ssh_password }}
|
||||
PROD_SSH_PASSWORD_SECRET: ${{ secrets.PROD_SSH_PASSWORD }}
|
||||
K3S_NAMESPACE: ${{ inputs.namespace }}
|
||||
REGISTRY_HOST: ${{ inputs.registry_host }}
|
||||
REGISTRY_OWNER: ${{ inputs.registry_owner }}
|
||||
REGISTRY_IMAGE_PREFIX: ${{ inputs.registry_image_prefix }}
|
||||
REGISTRY_USERNAME: ${{ inputs.registry_username }}
|
||||
REGISTRY_PASSWORD_SOURCE: ${{ inputs.registry_password_source }}
|
||||
REGISTRY_PASSWORD_INPUT: ${{ inputs.registry_password }}
|
||||
REGISTRY_PASSWORD_SECRET: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
REGISTRY_PLAIN_HTTP: ${{ inputs.registry_plain_http }}
|
||||
SELECTED_TARGET: ${{ inputs.target }}
|
||||
REQUESTED_IMAGE_TAG: ${{ inputs.image_tag }}
|
||||
REQUESTED_RUN_MIGRATIONS: ${{ inputs.run_migrations }}
|
||||
WAIT_TIMEOUT: ${{ inputs.wait_timeout }}
|
||||
DOCKER_BUILDKIT: "1"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install deployment tools
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y curl docker.io openssh-client sshpass tar
|
||||
|
||||
- name: Resolve deployment metadata
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
PROD_HOST="${PROD_HOST#ssh://}"
|
||||
PROD_HOST="${PROD_HOST%/}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST#http://}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST#https://}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST%/}"
|
||||
|
||||
case "${PROD_PORT}" in
|
||||
*[!0-9]*|"")
|
||||
echo "::error::Production SSH port must be numeric"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${PROD_HOST}" in
|
||||
*[!A-Za-z0-9_.:-]*|"")
|
||||
echo "::error::Production host must be a host or IP without scheme"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${PROD_USER}" in
|
||||
*[!A-Za-z0-9_.@-]*|"")
|
||||
echo "::error::Production SSH user contains unsupported characters"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${PROD_SSH_PASSWORD_SOURCE:-repository_secret}" in
|
||||
repository_secret)
|
||||
prod_ssh_password="${PROD_SSH_PASSWORD_SECRET:-}"
|
||||
;;
|
||||
workflow_input)
|
||||
prod_ssh_password="${PROD_SSH_PASSWORD_INPUT:-}"
|
||||
;;
|
||||
*)
|
||||
echo "::error::password_source must be repository_secret or workflow_input"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "${prod_ssh_password}" ]; then
|
||||
echo "::add-mask::${prod_ssh_password}"
|
||||
fi
|
||||
|
||||
if [ -z "${prod_ssh_password}" ]; then
|
||||
if [ "${PROD_SSH_PASSWORD_SOURCE:-repository_secret}" = "workflow_input" ]; then
|
||||
echo "::error::Missing workflow input ssh_password"
|
||||
else
|
||||
echo "::error::Missing repository secret PROD_SSH_PASSWORD"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${REGISTRY_PASSWORD_SOURCE:-repository_secret}" in
|
||||
repository_secret)
|
||||
registry_password="${REGISTRY_PASSWORD_SECRET:-}"
|
||||
;;
|
||||
workflow_input)
|
||||
registry_password="${REGISTRY_PASSWORD_INPUT:-}"
|
||||
;;
|
||||
*)
|
||||
echo "::error::registry_password_source must be repository_secret or workflow_input"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "${registry_password}" ]; then
|
||||
echo "::add-mask::${registry_password}"
|
||||
fi
|
||||
|
||||
if [ -z "${registry_password}" ]; then
|
||||
if [ "${REGISTRY_PASSWORD_SOURCE:-repository_secret}" = "workflow_input" ]; then
|
||||
echo "::error::Missing workflow input registry_password"
|
||||
else
|
||||
echo "::error::Missing repository secret REGISTRY_PASSWORD"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${REGISTRY_HOST}" in
|
||||
*[!A-Za-z0-9_.:-]*|"")
|
||||
echo "::error::registry_host must be a host[:port] without scheme"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${REGISTRY_OWNER}" in
|
||||
*[!A-Za-z0-9_.-]*|"")
|
||||
echo "::error::registry_owner contains unsupported characters"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${REGISTRY_IMAGE_PREFIX}" in
|
||||
*[!A-Za-z0-9_.-]*|"")
|
||||
echo "::error::registry_image_prefix contains unsupported characters"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${REGISTRY_USERNAME}" in
|
||||
*[!A-Za-z0-9_.@-]*|"")
|
||||
echo "::error::registry_username contains unsupported characters"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${REGISTRY_PLAIN_HTTP}" in
|
||||
true|false) ;;
|
||||
*)
|
||||
echo "::error::registry_plain_http must be true or false"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${K3S_NAMESPACE}" in
|
||||
*[!A-Za-z0-9_.-]*|"")
|
||||
echo "::error::namespace contains unsupported characters"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${WAIT_TIMEOUT}" in
|
||||
*[!A-Za-z0-9_.-]*|"")
|
||||
echo "::error::wait_timeout contains unsupported characters"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
all_app_images="tenant-api browser-fetch ops-api worker-generate kol-assist-worker scheduler frontend ops-web"
|
||||
backend_images="tenant-api browser-fetch ops-api worker-generate kol-assist-worker scheduler"
|
||||
deployment_list=""
|
||||
auto_migrations=false
|
||||
skip_deployments=false
|
||||
|
||||
case "${SELECTED_TARGET}" in
|
||||
frontend)
|
||||
deployment_list="frontend"
|
||||
;;
|
||||
ops-web)
|
||||
deployment_list="ops-web"
|
||||
;;
|
||||
browser-fetch)
|
||||
deployment_list="browser-fetch"
|
||||
;;
|
||||
tenant-api)
|
||||
deployment_list="tenant-api"
|
||||
auto_migrations=true
|
||||
;;
|
||||
ops-api)
|
||||
deployment_list="ops-api"
|
||||
auto_migrations=true
|
||||
;;
|
||||
worker-generate)
|
||||
deployment_list="worker-generate"
|
||||
auto_migrations=true
|
||||
;;
|
||||
kol-assist-worker)
|
||||
deployment_list="kol-assist-worker"
|
||||
auto_migrations=true
|
||||
;;
|
||||
scheduler)
|
||||
deployment_list="scheduler"
|
||||
auto_migrations=true
|
||||
;;
|
||||
web)
|
||||
deployment_list="frontend ops-web"
|
||||
;;
|
||||
backend)
|
||||
deployment_list="${backend_images}"
|
||||
auto_migrations=true
|
||||
;;
|
||||
all-apps)
|
||||
deployment_list="${all_app_images}"
|
||||
auto_migrations=true
|
||||
;;
|
||||
migrate)
|
||||
deployment_list=""
|
||||
auto_migrations=true
|
||||
skip_deployments=true
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unsupported target: ${SELECTED_TARGET}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${REQUESTED_RUN_MIGRATIONS}" in
|
||||
auto)
|
||||
run_migrations="${auto_migrations}"
|
||||
;;
|
||||
yes)
|
||||
run_migrations=true
|
||||
;;
|
||||
no)
|
||||
if [ "${SELECTED_TARGET}" = "migrate" ]; then
|
||||
echo "::error::target=migrate requires run_migrations=yes or auto"
|
||||
exit 1
|
||||
fi
|
||||
run_migrations=false
|
||||
;;
|
||||
*)
|
||||
echo "::error::run_migrations must be auto, yes, or no"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
image_list="${deployment_list}"
|
||||
if [ "${run_migrations}" = "true" ]; then
|
||||
case " ${image_list} " in
|
||||
*" migrate "*) ;;
|
||||
*) image_list="migrate ${image_list}" ;;
|
||||
esac
|
||||
fi
|
||||
image_list="$(printf '%s' "${image_list}" | xargs)"
|
||||
|
||||
image_tag="${REQUESTED_IMAGE_TAG}"
|
||||
if [ -z "${image_tag}" ]; then
|
||||
image_tag="$(git rev-parse --short=8 HEAD)"
|
||||
fi
|
||||
|
||||
case "${image_tag}" in
|
||||
*[!A-Za-z0-9_.-]*|"")
|
||||
echo "::error::image_tag may only contain letters, numbers, underscore, dot, and dash"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
{
|
||||
echo "TARGET_PASSWORD<<__TARGET_PASSWORD__"
|
||||
printf '%s\n' "${prod_ssh_password}"
|
||||
echo "__TARGET_PASSWORD__"
|
||||
echo "REGISTRY_PASSWORD<<__REGISTRY_PASSWORD__"
|
||||
printf '%s\n' "${registry_password}"
|
||||
echo "__REGISTRY_PASSWORD__"
|
||||
echo "PROD_SSH_PASSWORD_INPUT="
|
||||
echo "PROD_SSH_PASSWORD_SECRET="
|
||||
echo "REGISTRY_PASSWORD_INPUT="
|
||||
echo "REGISTRY_PASSWORD_SECRET="
|
||||
echo "PROD_HOST=${PROD_HOST}"
|
||||
echo "TARGET_HOST=${PROD_USER}@${PROD_HOST}"
|
||||
echo "TARGET_PORT=${PROD_PORT}"
|
||||
echo "NAMESPACE=${K3S_NAMESPACE}"
|
||||
echo "REGISTRY_HOST=${REGISTRY_HOST}"
|
||||
echo "REGISTRY_OWNER=${REGISTRY_OWNER}"
|
||||
echo "REGISTRY_IMAGE_PREFIX=${REGISTRY_IMAGE_PREFIX}"
|
||||
echo "REGISTRY_USERNAME=${REGISTRY_USERNAME}"
|
||||
echo "REGISTRY_PLAIN_HTTP=${REGISTRY_PLAIN_HTTP}"
|
||||
echo "PULL_FROM_REGISTRY=true"
|
||||
echo "IMAGE_TAG=${image_tag}"
|
||||
echo "IMAGE_LIST=${image_list}"
|
||||
echo "DEPLOYMENT_LIST=${deployment_list}"
|
||||
echo "RUN_MIGRATIONS=${run_migrations}"
|
||||
echo "SKIP_DEPLOYMENTS=${skip_deployments}"
|
||||
echo "WAIT_TIMEOUT=${WAIT_TIMEOUT}"
|
||||
} >> "${GITHUB_ENV}"
|
||||
|
||||
echo "Selected target: ${SELECTED_TARGET}"
|
||||
echo "Deployments to roll out: ${deployment_list:-none}"
|
||||
echo "Images to verify/import: ${image_list:-none}"
|
||||
echo "Run migrations: ${run_migrations}"
|
||||
echo "Image tag: ${image_tag}"
|
||||
echo "Production target: ${PROD_USER}@${PROD_HOST}:${PROD_PORT}"
|
||||
echo "Gitea Registry: ${REGISTRY_HOST}/${REGISTRY_OWNER}/${REGISTRY_IMAGE_PREFIX}/<service>:${image_tag}"
|
||||
|
||||
- name: Verify selected images in Gitea Registry
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
if [ -z "${IMAGE_LIST}" ]; then
|
||||
echo "No images selected; skipping registry verification."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! bash deploy/scripts/gitea-registry-images.sh verify; then
|
||||
echo "::error::Missing image(s) at tag '${IMAGE_TAG}' for: ${IMAGE_LIST}."
|
||||
echo "::error::Run Backend CI and/or Frontend CI manually first for the same commit/tag."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Prepare production SSH known host
|
||||
run: |
|
||||
set -eu
|
||||
mkdir -p ~/.ssh
|
||||
chmod 700 ~/.ssh
|
||||
ssh-keyscan -p "${TARGET_PORT}" "${PROD_HOST}" >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Import images and roll production k3s
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
echo "Deploying ${DEPLOYMENT_LIST:-migration only} with tag ${IMAGE_TAG}..."
|
||||
# Intentional word splitting: DEPLOYMENT_LIST is validated service names.
|
||||
bash deploy/scripts/deploy-images-to-k3s.sh ${DEPLOYMENT_LIST}
|
||||
Executable
+420
@@ -0,0 +1,420 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deploy already-built images to a remote k3s host.
|
||||
#
|
||||
# The script can either use local Docker images or pull images from the Gitea
|
||||
# registry, retag them as geo-rankly/<service>:<tag>, copy a Docker archive to
|
||||
# the production host, import it into k3s containerd, and roll deployments.
|
||||
#
|
||||
# Example:
|
||||
# IMAGE_TAG=20260531-abcdef12 \
|
||||
# PULL_FROM_REGISTRY=true REGISTRY_PASSWORD=... \
|
||||
# TARGET_PASSWORD=... \
|
||||
# bash deploy/scripts/deploy-images-to-k3s.sh ops-api ops-web frontend
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
TARGET_HOST="${TARGET_HOST:-root@39.105.229.239}"
|
||||
TARGET_PORT="${TARGET_PORT:-22}"
|
||||
TARGET_PASSWORD="${TARGET_PASSWORD:-}"
|
||||
NAMESPACE="${NAMESPACE:-geo-rankly}"
|
||||
LOCAL_PREFIX="${LOCAL_PREFIX:-geo-rankly}"
|
||||
IMAGE_TAG="${IMAGE_TAG:-}"
|
||||
REMOTE_DIR="${REMOTE_DIR:-/opt/geo-rankly/releases}"
|
||||
WAIT_TIMEOUT="${WAIT_TIMEOUT:-10m}"
|
||||
DELETE_REMOTE_TAR="${DELETE_REMOTE_TAR:-true}"
|
||||
PULL_FROM_REGISTRY="${PULL_FROM_REGISTRY:-false}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST:-192.168.100.19:13000}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST#http://}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST#https://}"
|
||||
REGISTRY_HOST="${REGISTRY_HOST%/}"
|
||||
REGISTRY_OWNER="${REGISTRY_OWNER:-root}"
|
||||
REGISTRY_IMAGE_PREFIX="${REGISTRY_IMAGE_PREFIX:-geo-rankly}"
|
||||
REGISTRY_USERNAME="${REGISTRY_USERNAME:-root}"
|
||||
REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}"
|
||||
REGISTRY_PLAIN_HTTP="${REGISTRY_PLAIN_HTTP:-true}"
|
||||
RUN_MIGRATIONS="${RUN_MIGRATIONS:-false}"
|
||||
SKIP_DEPLOYMENTS="${SKIP_DEPLOYMENTS:-false}"
|
||||
TARGET_PLATFORM="${TARGET_PLATFORM:-}"
|
||||
CLEAN_LOCAL_IMAGES="${CLEAN_LOCAL_IMAGES:-false}"
|
||||
|
||||
DEFAULT_SERVICES=(
|
||||
tenant-api
|
||||
browser-fetch
|
||||
ops-api
|
||||
worker-generate
|
||||
kol-assist-worker
|
||||
scheduler
|
||||
frontend
|
||||
ops-web
|
||||
)
|
||||
|
||||
DEPLOYABLE_SERVICES=(
|
||||
tenant-api
|
||||
browser-fetch
|
||||
ops-api
|
||||
worker-generate
|
||||
kol-assist-worker
|
||||
scheduler
|
||||
frontend
|
||||
ops-web
|
||||
)
|
||||
|
||||
if [[ -z "${IMAGE_TAG}" ]]; then
|
||||
echo "IMAGE_TAG is required. Use a unique tag, for example: IMAGE_TAG=$(git -C "${REPO_ROOT}" rev-parse --short=8 HEAD)-$(date +%Y%m%d%H%M%S)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$#" -gt 0 ]]; then
|
||||
SERVICES=("$@")
|
||||
elif [[ "${SKIP_DEPLOYMENTS}" == "true" ]]; then
|
||||
SERVICES=()
|
||||
else
|
||||
SERVICES=("${DEFAULT_SERVICES[@]}")
|
||||
fi
|
||||
|
||||
case "${IMAGE_TAG}" in
|
||||
*[!A-Za-z0-9_.-]*|"")
|
||||
echo "IMAGE_TAG may only contain letters, numbers, underscore, dot, and dash." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${TARGET_PORT}" in
|
||||
*[!0-9]*|"")
|
||||
echo "TARGET_PORT must be numeric." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${TARGET_HOST}" in
|
||||
*[!A-Za-z0-9_.@:-]*|"")
|
||||
echo "TARGET_HOST must be user@host or host without spaces/shell characters." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${REMOTE_DIR}" in
|
||||
/*) ;;
|
||||
*)
|
||||
echo "REMOTE_DIR must be an absolute path." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${REMOTE_DIR}" in
|
||||
*[!A-Za-z0-9_./-]*)
|
||||
echo "REMOTE_DIR contains unsupported characters." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${NAMESPACE}" in
|
||||
*[!A-Za-z0-9_.-]*|"")
|
||||
echo "NAMESPACE contains unsupported characters." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${LOCAL_PREFIX}" in
|
||||
*[!A-Za-z0-9_./-]*|"")
|
||||
echo "LOCAL_PREFIX contains unsupported characters." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${PULL_FROM_REGISTRY}" in
|
||||
true|false) ;;
|
||||
*)
|
||||
echo "PULL_FROM_REGISTRY must be true or false." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${RUN_MIGRATIONS}" in
|
||||
true|false) ;;
|
||||
*)
|
||||
echo "RUN_MIGRATIONS must be true or false." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${SKIP_DEPLOYMENTS}" in
|
||||
true|false) ;;
|
||||
*)
|
||||
echo "SKIP_DEPLOYMENTS must be true or false." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${DELETE_REMOTE_TAR}" in
|
||||
true|false) ;;
|
||||
*)
|
||||
echo "DELETE_REMOTE_TAR must be true or false." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${WAIT_TIMEOUT}" in
|
||||
*[!A-Za-z0-9_.-]*|"")
|
||||
echo "WAIT_TIMEOUT contains unsupported characters." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
supported=false
|
||||
for known in "${DEPLOYABLE_SERVICES[@]}"; do
|
||||
if [[ "${svc}" == "${known}" ]]; then
|
||||
supported=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "${supported}" != "true" ]]; then
|
||||
echo "Unsupported service ${svc}. Pass deployment/container names only." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
IMAGE_SERVICES=("${SERVICES[@]}")
|
||||
if [[ "${RUN_MIGRATIONS}" == "true" ]]; then
|
||||
IMAGE_SERVICES=(migrate "${IMAGE_SERVICES[@]}")
|
||||
fi
|
||||
|
||||
if [[ "${#IMAGE_SERVICES[@]}" -eq 0 ]]; then
|
||||
echo "No images selected. Pass services or set RUN_MIGRATIONS=true." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "docker is required on the NAS/build/Gitea runner host." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${TARGET_PASSWORD}" ]] && ! command -v sshpass >/dev/null 2>&1; then
|
||||
echo "sshpass is required when TARGET_PASSWORD is set." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
registry_image() {
|
||||
local svc="$1"
|
||||
printf '%s/%s/%s/%s:%s' \
|
||||
"${REGISTRY_HOST}" \
|
||||
"${REGISTRY_OWNER}" \
|
||||
"${REGISTRY_IMAGE_PREFIX}" \
|
||||
"${svc}" \
|
||||
"${IMAGE_TAG}"
|
||||
}
|
||||
|
||||
local_image() {
|
||||
local svc="$1"
|
||||
printf '%s/%s:%s' "${LOCAL_PREFIX}" "${svc}" "${IMAGE_TAG}"
|
||||
}
|
||||
|
||||
if [[ "${PULL_FROM_REGISTRY}" == "true" ]]; then
|
||||
for value_name in REGISTRY_HOST REGISTRY_OWNER REGISTRY_IMAGE_PREFIX REGISTRY_USERNAME; do
|
||||
value="${!value_name}"
|
||||
if [[ -z "${value}" ]]; then
|
||||
echo "${value_name} is required when PULL_FROM_REGISTRY=true." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "${REGISTRY_PASSWORD}" ]]; then
|
||||
echo "REGISTRY_PASSWORD is required when PULL_FROM_REGISTRY=true." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "${REGISTRY_PLAIN_HTTP}" in
|
||||
true|false) ;;
|
||||
*)
|
||||
echo "REGISTRY_PLAIN_HTTP must be true or false." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "${REGISTRY_PLAIN_HTTP}" == "true" ]] \
|
||||
&& ! docker info 2>/dev/null \
|
||||
| sed -n '/Insecure Registries:/,/Live Restore/p' \
|
||||
| grep -Fq "${REGISTRY_HOST}"; then
|
||||
echo "${REGISTRY_HOST} is not listed in Docker insecure registries." >&2
|
||||
echo "Configure the Gitea runner Docker daemon with: {\"insecure-registries\":[\"${REGISTRY_HOST}\"]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "${REGISTRY_PASSWORD}" \
|
||||
| docker login "${REGISTRY_HOST}" -u "${REGISTRY_USERNAME}" --password-stdin
|
||||
|
||||
for svc in "${IMAGE_SERVICES[@]}"; do
|
||||
src="$(registry_image "${svc}")"
|
||||
dst="$(local_image "${svc}")"
|
||||
echo "==> Pulling ${src}"
|
||||
if [[ -n "${TARGET_PLATFORM}" ]]; then
|
||||
docker pull --platform "${TARGET_PLATFORM}" "${src}"
|
||||
else
|
||||
docker pull "${src}"
|
||||
fi
|
||||
docker tag "${src}" "${dst}"
|
||||
done
|
||||
fi
|
||||
|
||||
images=()
|
||||
for svc in "${IMAGE_SERVICES[@]}"; do
|
||||
image="$(local_image "${svc}")"
|
||||
if ! docker image inspect "${image}" >/dev/null 2>&1; then
|
||||
echo "Missing local image: ${image}" >&2
|
||||
echo "Build it first, or run with PULL_FROM_REGISTRY=true and a tag that exists in Gitea." >&2
|
||||
exit 1
|
||||
fi
|
||||
images+=("${image}")
|
||||
done
|
||||
|
||||
safe_tag="$(printf '%s' "${IMAGE_TAG}" | tr -c 'A-Za-z0-9_.-' '-')"
|
||||
archive="/tmp/geo-rankly-images-${safe_tag}.tar"
|
||||
remote_archive="${REMOTE_DIR}/geo-rankly-images-${safe_tag}.tar"
|
||||
service_list="${SERVICES[*]}"
|
||||
|
||||
echo "==> Saving images:"
|
||||
printf ' %s\n' "${images[@]}"
|
||||
docker save "${images[@]}" -o "${archive}"
|
||||
echo " archive: ${archive} ($(du -sh "${archive}" | cut -f1))"
|
||||
|
||||
ssh_cmd=(ssh -p "${TARGET_PORT}" -o BatchMode=no -o StrictHostKeyChecking=accept-new)
|
||||
scp_cmd=(scp -P "${TARGET_PORT}" -o BatchMode=no -o StrictHostKeyChecking=accept-new)
|
||||
|
||||
remote_ssh() {
|
||||
if [[ -n "${TARGET_PASSWORD}" ]]; then
|
||||
SSHPASS="${TARGET_PASSWORD}" sshpass -e "${ssh_cmd[@]}" "${TARGET_HOST}" "$@"
|
||||
else
|
||||
"${ssh_cmd[@]}" "${TARGET_HOST}" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
remote_scp() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
if [[ -n "${TARGET_PASSWORD}" ]]; then
|
||||
SSHPASS="${TARGET_PASSWORD}" sshpass -e "${scp_cmd[@]}" "${source}" "${TARGET_HOST}:${target}"
|
||||
else
|
||||
"${scp_cmd[@]}" "${source}" "${TARGET_HOST}:${target}"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "==> Uploading archive to ${TARGET_HOST}:${remote_archive}"
|
||||
remote_ssh "mkdir -p '${REMOTE_DIR}'"
|
||||
remote_scp "${archive}" "${remote_archive}"
|
||||
|
||||
echo "==> Importing images and rolling deployments on ${TARGET_HOST}"
|
||||
remote_ssh \
|
||||
"NAMESPACE='${NAMESPACE}' LOCAL_PREFIX='${LOCAL_PREFIX}' IMAGE_TAG='${IMAGE_TAG}' SERVICES='${service_list}' REMOTE_ARCHIVE='${remote_archive}' WAIT_TIMEOUT='${WAIT_TIMEOUT}' DELETE_REMOTE_TAR='${DELETE_REMOTE_TAR}' RUN_MIGRATIONS='${RUN_MIGRATIONS}' bash -s" <<'REMOTE'
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v k3s >/dev/null 2>&1; then
|
||||
echo "k3s is required on the target host." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kubectl_cmd() {
|
||||
k3s kubectl "$@"
|
||||
}
|
||||
|
||||
echo " importing ${REMOTE_ARCHIVE}"
|
||||
k3s ctr -n k8s.io images import "${REMOTE_ARCHIVE}"
|
||||
|
||||
if [[ "${RUN_MIGRATIONS}" == "true" ]]; then
|
||||
migrate_image="${LOCAL_PREFIX}/migrate:${IMAGE_TAG}"
|
||||
echo " running migration job with ${migrate_image}"
|
||||
kubectl_cmd -n "${NAMESPACE}" delete job migrate --ignore-not-found
|
||||
cat <<EOF | kubectl_cmd -n "${NAMESPACE}" apply -f -
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: migrate
|
||||
labels:
|
||||
app.kubernetes.io/name: migrate
|
||||
spec:
|
||||
backoffLimit: 6
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: migrate
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
containers:
|
||||
- name: migrate
|
||||
image: ${migrate_image}
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: CONFIG_PATH
|
||||
value: /app/configs/config.yaml
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: geo-rankly-app-secret
|
||||
key: POSTGRES_PASSWORD
|
||||
volumeMounts:
|
||||
- name: app-config
|
||||
mountPath: /app/configs
|
||||
readOnly: true
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
export PGPASSWORD="\${POSTGRES_PASSWORD}"
|
||||
/usr/local/bin/migrate -path /migrations -database "postgres://geo@postgres:5432/geo?sslmode=disable" up
|
||||
/usr/local/bin/migrate -path /migrations_monitoring -database "postgres://geo@monitoring-postgres:5432/geo_monitoring?sslmode=disable" up
|
||||
/usr/local/bin/migrate -path /migrations_ops -database "postgres://geo@postgres:5432/geo?sslmode=disable&x-migrations-table=schema_migrations_ops" up
|
||||
/usr/local/bin/seed-platform-templates
|
||||
volumes:
|
||||
- name: app-config
|
||||
projected:
|
||||
sources:
|
||||
- configMap:
|
||||
name: geo-rankly-app-config
|
||||
items:
|
||||
- key: config.yaml
|
||||
path: config.yaml
|
||||
- key: prompts.yml
|
||||
path: prompts.yml
|
||||
- secret:
|
||||
name: geo-rankly-app-secret
|
||||
items:
|
||||
- key: config.local.yaml
|
||||
path: config.local.yaml
|
||||
EOF
|
||||
if ! kubectl_cmd -n "${NAMESPACE}" wait --for=condition=complete job/migrate --timeout="${WAIT_TIMEOUT}"; then
|
||||
kubectl_cmd -n "${NAMESPACE}" logs job/migrate --tail=200 || true
|
||||
kubectl_cmd -n "${NAMESPACE}" describe job migrate || true
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
for svc in ${SERVICES}; do
|
||||
image="${LOCAL_PREFIX}/${svc}:${IMAGE_TAG}"
|
||||
echo " deployment/${svc} -> ${image}"
|
||||
kubectl_cmd -n "${NAMESPACE}" set image "deployment/${svc}" "${svc}=${image}"
|
||||
kubectl_cmd -n "${NAMESPACE}" rollout restart "deployment/${svc}"
|
||||
done
|
||||
|
||||
for svc in ${SERVICES}; do
|
||||
echo " waiting deployment/${svc}"
|
||||
kubectl_cmd -n "${NAMESPACE}" rollout status "deployment/${svc}" --timeout="${WAIT_TIMEOUT}"
|
||||
done
|
||||
|
||||
if [[ -n "${SERVICES}" ]]; then
|
||||
kubectl_cmd -n "${NAMESPACE}" get deploy ${SERVICES}
|
||||
fi
|
||||
|
||||
if [[ "${DELETE_REMOTE_TAR}" == "true" ]]; then
|
||||
rm -f "${REMOTE_ARCHIVE}"
|
||||
fi
|
||||
REMOTE
|
||||
|
||||
rm -f "${archive}"
|
||||
|
||||
if [[ "${CLEAN_LOCAL_IMAGES}" == "true" ]]; then
|
||||
docker image rm "${images[@]}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "k3s image rollout complete: ${IMAGE_TAG}"
|
||||
Reference in New Issue
Block a user