Files
easyai-ai-gateway/scripts/cluster/bootstrap-platform.sh
T
wangbo 9e4fc7362d feat(queue): 增加非文本模型分布式准入队列
使用 PostgreSQL 统一同步与异步非文本任务的并发准入、持久化等待和 Worker 容量分配,并将生产 API 与独立 Worker 角色拆分。

补充策略管理、共享契约、OpenAPI、Kubernetes 双节点 Worker 清单及跨节点验收脚本;未默认启用任何生产 queue_size 策略。

已在原基线完成 Go、前端、迁移、Shell、Kustomize 与长任务容量验收;合入最新主干后将重新执行发布门禁。
2026-07-29 16:15:43 +08:00

279 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=scripts/cluster/common.sh
source "$script_dir/common.sh"
mode=${1:-}
release_manifest=${2:-}
[[ $mode == prepare || $mode == activate ]] || {
echo 'usage: bootstrap-platform.sh {prepare|activate} dist/releases/<sha>.json' >&2
exit 64
}
[[ -f $release_manifest && ! -L $release_manifest ]] || {
echo 'release manifest must be a regular file' >&2
exit 1
}
load_cluster_env
require_commands curl kubectl node openssl rg shasum
node "$cluster_root/scripts/release-manifest.mjs" validate "$release_manifest" >/dev/null
api_image=$(node "$cluster_root/scripts/release-manifest.mjs" get "$release_manifest" images.api)
web_image=$(node "$cluster_root/scripts/release-manifest.mjs" get "$release_manifest" images.web)
source_sha=$(node "$cluster_root/scripts/release-manifest.mjs" get "$release_manifest" sourceSha)
activate_workers=${AI_GATEWAY_ACTIVATE_WORKERS:-true}
[[ $activate_workers == true || $activate_workers == false ]] || {
echo 'AI_GATEWAY_ACTIVATE_WORKERS must be true or false' >&2
exit 64
}
[[ $api_image =~ ^registry\.cn-shanghai\.aliyuncs\.com/easyaigc/ai-gateway@sha256:[0-9a-f]{64}$ ]]
[[ $web_image =~ ^registry\.cn-shanghai\.aliyuncs\.com/easyaigc/ai-gateway-web@sha256:[0-9a-f]{64}$ ]]
secret_root="$cluster_root/.local-secrets/cluster"
postgres_environment="$secret_root/postgres.env"
working_directory=$(mktemp -d "$secret_root/platform.XXXXXX")
trap '[[ $working_directory == "$secret_root"/platform.* ]] && rm -rf -- "$working_directory"' EXIT HUP INT TERM
install -d -m 0700 "$secret_root"
if [[ ! -f $postgres_environment ]]; then
umask 077
printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 32)" >"$postgres_environment"
chmod 0600 "$postgres_environment"
fi
assert_private_file "$postgres_environment"
# shellcheck source=/dev/null
source "$postgres_environment"
: "${POSTGRES_PASSWORD:?}"
[[ $POSTGRES_PASSWORD =~ ^[0-9a-f]{64}$ ]] || {
echo 'invalid generated PostgreSQL password' >&2
exit 1
}
operators=(cert-manager.yaml cnpg.yaml barman.yaml)
operator_urls=(
https://github.com/cert-manager/cert-manager/releases/download/v1.21.0/cert-manager.yaml
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.29/releases/cnpg-1.29.1.yaml
https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.13.0/manifest.yaml
)
operator_checksums=(
6e499c3f1ab356abe79a7853911f80cb09c213885bfdf81092fdff142ba63c4a
e0c5ff41bf5b01c0775bf225929b8423d543cd23e9742e4274fe7de8499cf93a
d2e71e7b06822448f1a421f05781846cfdb9cc621e7ef32eef5e20c5133213b0
)
if [[ $mode == prepare ]]; then
for index in 0 1 2; do
operator=${operators[$index]}
curl -fsSL "${operator_urls[$index]}" -o "$working_directory/$operator"
actual_checksum=$(shasum -a 256 "$working_directory/$operator" | awk '{print $1}')
[[ $actual_checksum == "${operator_checksums[$index]}" ]] || {
echo "operator manifest checksum mismatch: $operator" >&2
exit 1
}
done
fi
cp -R "$cluster_root/deploy/kubernetes/production" "$working_directory/production"
api_digest=${api_image##*@}
web_digest=${web_image##*@}
awk -v api_digest="$api_digest" -v web_digest="$web_digest" '
$3 == "easyai-api" { image = "api" }
$3 == "easyai-web" { image = "web" }
$1 == "digest:" && image == "api" { print " digest: " api_digest; image = ""; next }
$1 == "digest:" && image == "web" { print " digest: " web_digest; image = ""; next }
{ print }
' "$working_directory/production/kustomization.yaml" \
>"$working_directory/production/kustomization.yaml.next"
mv "$working_directory/production/kustomization.yaml.next" \
"$working_directory/production/kustomization.yaml"
if [[ $mode == prepare ]]; then
sed -i.bak 's/^ replicas: 1$/ replicas: 0/' "$working_directory/production/application.yaml"
rm -f -- "$working_directory/production/application.yaml.bak"
fi
if [[ $activate_workers == false ]]; then
awk '
/^---$/ {
document_kind = ""
worker_deployment = 0
print
next
}
/^kind: / {
document_kind = $2
}
document_kind == "Deployment" && /^ name: easyai-worker-/ {
worker_deployment = 1
}
worker_deployment && /^ replicas: 1$/ {
sub(/1$/, "0")
}
{ print }
' "$working_directory/production/application.yaml" \
>"$working_directory/production/application.yaml.next"
mv "$working_directory/production/application.yaml.next" \
"$working_directory/production/application.yaml"
fi
kubectl kustomize "$working_directory/production" >"$working_directory/easyai-production.yaml"
if rg -q 'sha256:0{64}|image: (easyai-api|easyai-web)$' "$working_directory/easyai-production.yaml"; then
echo 'application images were not digest-pinned during rendering' >&2
exit 1
fi
if rg -i -q '^[[:space:]]*(password|secret|token|credential|private.?key):[[:space:]]+[^<{[:space:]]' \
"$working_directory/easyai-production.yaml"; then
echo 'rendered production manifests appear to contain a literal secret' >&2
exit 1
fi
umask 077
cat >"$working_directory/bootstrap-secrets.yaml" <<EOF
apiVersion: v1
kind: Secret
metadata:
name: easyai-postgres-app
namespace: easyai
type: kubernetes.io/basic-auth
stringData:
username: easyai
password: ${POSTGRES_PASSWORD}
---
apiVersion: v1
kind: Secret
metadata:
name: easyai-oss-backup
namespace: easyai
type: Opaque
stringData:
ACCESS_KEY_ID: ${ALI_KEY}
ACCESS_SECRET_KEY: ${ALI_SECRET}
EOF
chmod 0600 "$working_directory/bootstrap-secrets.yaml"
platform_files=(easyai-production.yaml bootstrap-secrets.yaml)
if [[ $mode == prepare ]]; then
platform_files=(cert-manager.yaml cnpg.yaml barman.yaml "${platform_files[@]}")
fi
for file in "${platform_files[@]}"; do
cluster_scp "$working_directory/$file" "$CLUSTER_NINGBO_HOST:/root/$file"
done
cluster_ssh "$CLUSTER_NINGBO_HOST" bash -s -- \
"$source_sha" "$mode" "$activate_workers" <<'REMOTE'
set -euo pipefail
source_sha=$1
mode=$2
activate_workers=$3
kubectl='k3s kubectl'
runtime_environment=
cleanup_bootstrap_files() {
if [[ -n ${runtime_environment:-} && -f $runtime_environment &&
! -L $runtime_environment ]]; then
shred -u "$runtime_environment"
fi
if [[ -f /root/bootstrap-secrets.yaml && ! -L /root/bootstrap-secrets.yaml ]]; then
shred -u /root/bootstrap-secrets.yaml
fi
for path in /root/cert-manager.yaml /root/cnpg.yaml /root/barman.yaml \
/root/easyai-production.yaml; do
[[ ! -e $path && ! -L $path ]] || unlink "$path"
done
}
trap cleanup_bootstrap_files EXIT HUP INT TERM
if [[ $mode == prepare ]]; then
$kubectl apply --server-side --force-conflicts -f /root/cert-manager.yaml >/dev/null
$kubectl rollout status deployment/cert-manager -n cert-manager --timeout=180s
$kubectl rollout status deployment/cert-manager-webhook -n cert-manager --timeout=180s
$kubectl rollout status deployment/cert-manager-cainjector -n cert-manager --timeout=180s
$kubectl apply --server-side --force-conflicts -f /root/cnpg.yaml >/dev/null
$kubectl rollout status deployment/cnpg-controller-manager -n cnpg-system --timeout=600s
$kubectl apply --server-side --force-conflicts -f /root/barman.yaml >/dev/null
$kubectl rollout status deployment/barman-cloud -n cnpg-system --timeout=600s
fi
$kubectl create namespace easyai --dry-run=client -o yaml | $kubectl apply -f - >/dev/null
$kubectl apply -f /root/bootstrap-secrets.yaml >/dev/null
postgres_password=$($kubectl get secret easyai-postgres-app -n easyai \
-o jsonpath='{.data.password}' | base64 -d)
runtime_environment=$(mktemp)
chmod 0600 "$runtime_environment"
docker inspect easyai-ai-gateway-api-1 \
--format '{{range .Config.Env}}{{println .}}{{end}}' |
grep -E '^(CONFIG_JWT_SECRET|SERVER_MAIN_INTERNAL_TOKEN|SERVER_MAIN_INTERNAL_KEY|SERVER_MAIN_INTERNAL_SECRET)=' \
>"$runtime_environment"
printf 'AI_GATEWAY_DATABASE_URL=postgresql://easyai:%s@easyai-postgres-rw:5432/easyai_ai_gateway?sslmode=require\n' \
"$postgres_password" >>"$runtime_environment"
[[ $(grep -c '^[A-Z0-9_]*=' "$runtime_environment") -eq 5 ]] || {
echo 'runtime Secret allowlist is incomplete' >&2
exit 1
}
$kubectl create secret generic easyai-ai-gateway-runtime -n easyai \
--from-env-file="$runtime_environment" --dry-run=client -o yaml |
$kubectl apply -f - >/dev/null
identity_directory=/var/lib/docker/volumes/easyai-ai-gateway_api_data/_data/identity/secrets
[[ -d $identity_directory && ! -L $identity_directory ]] || {
echo 'legacy identity SecretStore directory is unavailable' >&2
exit 1
}
mapfile -d '' identity_files < <(find "$identity_directory" -maxdepth 1 -type f -print0)
[[ ${#identity_files[@]} -eq 4 ]] || {
echo 'identity SecretStore file count changed from the accepted baseline' >&2
exit 1
}
identity_bytes=0
identity_arguments=()
for identity_file in "${identity_files[@]}"; do
identity_name=$(basename "$identity_file")
[[ $identity_name =~ ^[A-Za-z0-9._-]+$ ]] || {
echo 'identity SecretStore contains an invalid key name' >&2
exit 1
}
identity_bytes=$((identity_bytes + $(stat -c '%s' "$identity_file")))
identity_arguments+=("--from-file=$identity_name=$identity_file")
done
[[ $identity_bytes -eq 247 ]] || {
echo 'identity SecretStore byte count changed from the accepted baseline' >&2
exit 1
}
$kubectl create secret generic easyai-gateway-security-events -n easyai \
"${identity_arguments[@]}" --dry-run=client -o yaml |
$kubectl apply -f - >/dev/null
registry_config=/root/.docker/config.json
[[ -f $registry_config && ! -L $registry_config ]] || {
echo 'Docker registry config is unavailable on Ningbo' >&2
exit 1
}
$kubectl create secret generic easyai-registry -n easyai \
--type=kubernetes.io/dockerconfigjson \
--from-file=.dockerconfigjson="$registry_config" --dry-run=client -o yaml |
$kubectl apply -f - >/dev/null
$kubectl apply --server-side --dry-run=server --force-conflicts \
--field-manager=easyai-production-preflight \
-f /root/easyai-production.yaml >/dev/null
$kubectl apply -f /root/easyai-production.yaml >/dev/null
$kubectl annotate namespace easyai easyai.io/release="$source_sha" --overwrite >/dev/null
if [[ $mode == activate ]]; then
if [[ $activate_workers == true ]]; then
$kubectl rollout status deployment/easyai-worker-hongkong -n easyai --timeout=300s
$kubectl rollout status deployment/easyai-worker-ningbo -n easyai --timeout=300s
fi
$kubectl rollout status deployment/easyai-api-hongkong -n easyai --timeout=300s
$kubectl rollout status deployment/easyai-web-hongkong -n easyai --timeout=300s
$kubectl rollout status deployment/easyai-api-ningbo -n easyai --timeout=300s
$kubectl rollout status deployment/easyai-web-ningbo -n easyai --timeout=300s
fi
REMOTE
if [[ $mode == prepare ]]; then
cluster_ssh "$CLUSTER_NINGBO_HOST" \
'k3s kubectl wait --for=condition=Ready cluster/easyai-postgres -n easyai --timeout=600s'
fi
echo "platform_bootstrap=PASS mode=$mode release=$source_sha"