Proof: The rendered Kubernetes manifest now includes a Traefik TLS ingress for operator-dashboard and production basic auth, deploy workflow/bootstrap preserve the dashboard password as a secret, and static plus full node tests pass. Assumptions: doran.133011.xyz is the intended public host because unrip.doran.133011.xyz and dashboard.doran.133011.xyz do not currently resolve. Still fake: the public dashboard is not deployed or externally verified yet because the cluster host, Forgejo, and Kubernetes API timed out from this machine during this turn.
276 lines
9.3 KiB
Bash
Executable file
276 lines
9.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR=$(cd "$(dirname "$0")/../.." && pwd)
|
|
PLATFORM_REPO_DIR="${PLATFORM_REPO_DIR:-$ROOT_DIR/../unrip3}"
|
|
BOOTSTRAP_ENV_FILE="${BOOTSTRAP_ENV_FILE:-$PLATFORM_REPO_DIR/.state/hetzner/bootstrap-secrets.resolved.env}"
|
|
PLATFORM_APP_ENV_FILE="${PLATFORM_APP_ENV_FILE:-$PLATFORM_REPO_DIR/.env}"
|
|
APP_ENV_FILE="${APP_ENV_FILE:-$ROOT_DIR/.env}"
|
|
FORGEJO_REMOTE_NAME="${FORGEJO_REMOTE_NAME:-forgejo}"
|
|
|
|
PROJECT_NAME="${PROJECT_NAME:-unrip}"
|
|
PROJECT_NAMESPACE="${PROJECT_NAMESPACE:-$PROJECT_NAME}"
|
|
PROJECT_DEPLOYMENTS="${PROJECT_DEPLOYMENTS:-$(python3 "$ROOT_DIR/scripts/deploy/repo_deployments.py" --format csv)}"
|
|
PROJECT_REGISTRY_SECRET_NAME="${PROJECT_REGISTRY_SECRET_NAME:-${PROJECT_NAME}-registry-creds}"
|
|
APP_SECRET_NAME="${APP_SECRET_NAME:-${PROJECT_NAME}-secrets}"
|
|
SYNC_FORGEJO_REMOTE="${SYNC_FORGEJO_REMOTE:-1}"
|
|
|
|
require() {
|
|
command -v "$1" >/dev/null 2>&1 || {
|
|
echo "missing required command: $1" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
load_env_defaults() {
|
|
local file="$1"
|
|
[[ -f "$file" ]] || return 0
|
|
|
|
eval "$(
|
|
python3 - "$file" <<'PY'
|
|
import os
|
|
import shlex
|
|
import sys
|
|
|
|
for raw in open(sys.argv[1], 'r', encoding='utf-8'):
|
|
line = raw.strip()
|
|
if not line or line.startswith('#'):
|
|
continue
|
|
if line.startswith('export '):
|
|
line = line[len('export '):]
|
|
if '=' not in line:
|
|
continue
|
|
key, value = line.split('=', 1)
|
|
key = key.strip()
|
|
if key in os.environ:
|
|
continue
|
|
print(f'export {key}={shlex.quote(value)}')
|
|
PY
|
|
)"
|
|
}
|
|
|
|
require git
|
|
require kubectl
|
|
require python3
|
|
require base64
|
|
|
|
load_env_defaults "$BOOTSTRAP_ENV_FILE"
|
|
load_env_defaults "$PLATFORM_APP_ENV_FILE"
|
|
load_env_defaults "$APP_ENV_FILE"
|
|
|
|
REMOTE_URL="${FORGEJO_REMOTE_URL:-$(git -C "$ROOT_DIR" remote get-url "$FORGEJO_REMOTE_NAME")}"
|
|
eval "$(
|
|
python3 - "$REMOTE_URL" <<'PY'
|
|
import sys
|
|
from urllib.parse import urlparse
|
|
|
|
remote = sys.argv[1]
|
|
parsed = urlparse(remote)
|
|
if parsed.scheme not in {'http', 'https'}:
|
|
raise SystemExit('forgejo remote must use http(s) for automatic bootstrap')
|
|
|
|
path = parsed.path.rstrip('/')
|
|
if path.endswith('.git'):
|
|
path = path[:-4]
|
|
parts = [part for part in path.split('/') if part]
|
|
if len(parts) < 2:
|
|
raise SystemExit('could not parse repo owner/name from forgejo remote')
|
|
|
|
base_url = f'{parsed.scheme}://{parsed.hostname}'
|
|
if parsed.port:
|
|
base_url += f':{parsed.port}'
|
|
|
|
if parsed.username and parsed.password:
|
|
print(f'export FORGEJO_API_PASSWORD={parsed.password}')
|
|
|
|
if parsed.username:
|
|
print(f'export FORGEJO_API_USERNAME={parsed.username}')
|
|
|
|
print(f'export FORGEJO_URL={base_url}')
|
|
print(f'export FORGEJO_REPO_OWNER={parts[-2]}')
|
|
print(f'export FORGEJO_REPO_NAME={parts[-1]}')
|
|
PY
|
|
)"
|
|
|
|
: "${KUBECONFIG_PATH:=${KUBECONFIG:-$PLATFORM_REPO_DIR/.state/hetzner/kubeconfig.yaml}}"
|
|
: "${CI_KUBECONFIG_PATH:=$PLATFORM_REPO_DIR/.state/hetzner/kubeconfig.incluster.yaml}"
|
|
: "${FORGEJO_URL:?set FORGEJO_URL or configure a forgejo remote}"
|
|
: "${FORGEJO_REPO_OWNER:?set FORGEJO_REPO_OWNER}"
|
|
: "${FORGEJO_REPO_NAME:?set FORGEJO_REPO_NAME}"
|
|
: "${FORGEJO_ADMIN_USERNAME:=${FORGEJO_API_USERNAME:-}}"
|
|
|
|
if [[ -z "${FORGEJO_TOKEN:-}" ]]; then
|
|
: "${FORGEJO_ADMIN_USERNAME:=${FORGEJO_API_USERNAME:-}}"
|
|
: "${FORGEJO_ADMIN_USERNAME:?set FORGEJO_TOKEN or FORGEJO_ADMIN_USERNAME}"
|
|
: "${FORGEJO_ADMIN_PASSWORD:=${FORGEJO_API_PASSWORD:-}}"
|
|
: "${FORGEJO_ADMIN_PASSWORD:?set FORGEJO_TOKEN or FORGEJO_ADMIN_PASSWORD}"
|
|
fi
|
|
|
|
if [[ ! -f "$KUBECONFIG_PATH" ]]; then
|
|
echo "missing kubeconfig: $KUBECONFIG_PATH" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$CI_KUBECONFIG_PATH" ]]; then
|
|
echo "missing in-cluster kubeconfig: $CI_KUBECONFIG_PATH" >&2
|
|
exit 1
|
|
fi
|
|
|
|
export KUBECONFIG="$KUBECONFIG_PATH"
|
|
|
|
if [[ -z "${REGISTRY_HOST:-}" ]]; then
|
|
REGISTRY_HOST="${REGISTRY_DOMAIN:-}"
|
|
fi
|
|
if [[ -z "${REGISTRY_HOST:-}" ]]; then
|
|
REGISTRY_HOST="$(kubectl get ingress -n registry registry -o jsonpath='{.spec.rules[0].host}' 2>/dev/null || true)"
|
|
fi
|
|
if [[ -z "${REGISTRY_USERNAME:-}" ]]; then
|
|
REGISTRY_USERNAME="$(kubectl get secret registry-secrets -n registry -o jsonpath='{.data.htpasswd}' 2>/dev/null | base64 -d | cut -d: -f1 || true)"
|
|
fi
|
|
|
|
: "${REGISTRY_HOST:?set REGISTRY_HOST or configure the registry ingress first}"
|
|
: "${REGISTRY_USERNAME:?set REGISTRY_USERNAME or bootstrap the shared registry first}"
|
|
: "${REGISTRY_PASSWORD:?set REGISTRY_PASSWORD}"
|
|
: "${NEAR_INTENTS_API_KEY:?set NEAR_INTENTS_API_KEY}"
|
|
: "${POSTGRES_PASSWORD:=}"
|
|
: "${POSTGRES_URL:=}"
|
|
: "${NEAR_INTENTS_SIGNER_PRIVATE_KEY:=}"
|
|
: "${NOTIFICATION_NTFY_TOKEN:=}"
|
|
: "${OPERATOR_DASHBOARD_AUTH_PASSWORD:=}"
|
|
|
|
secret_value() {
|
|
local key="$1"
|
|
kubectl -n "$PROJECT_NAMESPACE" get secret "$APP_SECRET_NAME" -o "jsonpath={.data.${key}}" 2>/dev/null | base64 -d 2>/dev/null || true
|
|
}
|
|
|
|
if [[ -z "$POSTGRES_PASSWORD" ]]; then
|
|
POSTGRES_PASSWORD="$(secret_value POSTGRES_PASSWORD)"
|
|
fi
|
|
|
|
if [[ -z "$POSTGRES_URL" ]]; then
|
|
POSTGRES_URL="$(secret_value POSTGRES_URL)"
|
|
fi
|
|
|
|
if [[ -z "$NEAR_INTENTS_SIGNER_PRIVATE_KEY" ]]; then
|
|
NEAR_INTENTS_SIGNER_PRIVATE_KEY="$(secret_value NEAR_INTENTS_SIGNER_PRIVATE_KEY)"
|
|
fi
|
|
|
|
if [[ -z "$NOTIFICATION_NTFY_TOKEN" ]]; then
|
|
NOTIFICATION_NTFY_TOKEN="$(secret_value NOTIFICATION_NTFY_TOKEN)"
|
|
fi
|
|
|
|
if [[ -z "$OPERATOR_DASHBOARD_AUTH_PASSWORD" ]]; then
|
|
OPERATOR_DASHBOARD_AUTH_PASSWORD="$(secret_value OPERATOR_DASHBOARD_AUTH_PASSWORD)"
|
|
fi
|
|
|
|
if [[ -z "$POSTGRES_PASSWORD" ]]; then
|
|
POSTGRES_PASSWORD="$(python3 - <<'PY'
|
|
import secrets
|
|
print(secrets.token_urlsafe(24))
|
|
PY
|
|
)"
|
|
fi
|
|
|
|
if [[ -z "$POSTGRES_URL" ]]; then
|
|
POSTGRES_URL="postgresql://unrip:${POSTGRES_PASSWORD}@postgres:5432/unrip"
|
|
fi
|
|
|
|
: "${OPERATOR_DASHBOARD_AUTH_PASSWORD:?set OPERATOR_DASHBOARD_AUTH_PASSWORD or pre-create OPERATOR_DASHBOARD_AUTH_PASSWORD in $APP_SECRET_NAME}"
|
|
|
|
echo "bootstrapping namespace $PROJECT_NAMESPACE"
|
|
kubectl apply -f "$ROOT_DIR/deploy/k8s/base/namespace.yaml"
|
|
|
|
echo "upserting runtime secret $APP_SECRET_NAME"
|
|
secret_args=(
|
|
--from-literal=NEAR_INTENTS_API_KEY="$NEAR_INTENTS_API_KEY"
|
|
--from-literal=POSTGRES_PASSWORD="$POSTGRES_PASSWORD"
|
|
--from-literal=POSTGRES_URL="$POSTGRES_URL"
|
|
--from-literal=OPERATOR_DASHBOARD_AUTH_PASSWORD="$OPERATOR_DASHBOARD_AUTH_PASSWORD"
|
|
)
|
|
if [[ -n "$NEAR_INTENTS_SIGNER_PRIVATE_KEY" ]]; then
|
|
secret_args+=(--from-literal=NEAR_INTENTS_SIGNER_PRIVATE_KEY="$NEAR_INTENTS_SIGNER_PRIVATE_KEY")
|
|
fi
|
|
if [[ -n "$NOTIFICATION_NTFY_TOKEN" ]]; then
|
|
secret_args+=(--from-literal=NOTIFICATION_NTFY_TOKEN="$NOTIFICATION_NTFY_TOKEN")
|
|
fi
|
|
kubectl -n "$PROJECT_NAMESPACE" create secret generic "$APP_SECRET_NAME" \
|
|
"${secret_args[@]}" \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
echo "upserting registry pull/push secret $PROJECT_REGISTRY_SECRET_NAME"
|
|
kubectl -n "$PROJECT_NAMESPACE" create secret docker-registry "$PROJECT_REGISTRY_SECRET_NAME" \
|
|
--docker-server="$REGISTRY_HOST" \
|
|
--docker-username="$REGISTRY_USERNAME" \
|
|
--docker-password="$REGISTRY_PASSWORD" \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
|
|
current_release_image() {
|
|
kubectl -n "$PROJECT_NAMESPACE" get deploy operator-dashboard \
|
|
-o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || true
|
|
}
|
|
|
|
APP_MANIFEST_IMAGE="${PROJECT_RELEASE_IMAGE:-$(current_release_image)}"
|
|
BOOTSTRAP_IMAGE="ghcr.io/example/unrip:bootstrap"
|
|
|
|
echo "applying app manifests"
|
|
if [[ -n "$APP_MANIFEST_IMAGE" && "$APP_MANIFEST_IMAGE" != "$BOOTSTRAP_IMAGE" ]]; then
|
|
kubectl kustomize "$ROOT_DIR/deploy/k8s/base" \
|
|
| python3 "$ROOT_DIR/scripts/deploy/render_release_manifest.py" --image "$APP_MANIFEST_IMAGE" \
|
|
| kubectl apply -f -
|
|
else
|
|
kubectl apply -k "$ROOT_DIR/deploy/k8s/base"
|
|
fi
|
|
|
|
echo "upserting Forgejo repo settings"
|
|
forgejo_args=()
|
|
if [[ -n "${FORGEJO_TOKEN:-}" ]]; then
|
|
forgejo_args+=(--token "$FORGEJO_TOKEN")
|
|
fi
|
|
if [[ -n "${FORGEJO_ADMIN_USERNAME:-}" ]]; then
|
|
forgejo_args+=(--admin-username "$FORGEJO_ADMIN_USERNAME")
|
|
fi
|
|
if [[ -n "${FORGEJO_ADMIN_PASSWORD:-}" ]]; then
|
|
forgejo_args+=(--admin-password "$FORGEJO_ADMIN_PASSWORD")
|
|
fi
|
|
if [[ -n "${OPERATOR_DASHBOARD_AUTH_PASSWORD:-}" ]]; then
|
|
forgejo_args+=(--operator-dashboard-auth-password "$OPERATOR_DASHBOARD_AUTH_PASSWORD")
|
|
fi
|
|
|
|
python3 "$ROOT_DIR/scripts/deploy/forgejo_repo_bootstrap.py" \
|
|
--forgejo-url "$FORGEJO_URL" \
|
|
--repo-owner "$FORGEJO_REPO_OWNER" \
|
|
--repo-name "$FORGEJO_REPO_NAME" \
|
|
--ci-kubeconfig "$CI_KUBECONFIG_PATH" \
|
|
--registry-host "$REGISTRY_HOST" \
|
|
--project-name "$PROJECT_NAME" \
|
|
--project-namespace "$PROJECT_NAMESPACE" \
|
|
--project-deployments "$PROJECT_DEPLOYMENTS" \
|
|
--project-registry-secret-name "$PROJECT_REGISTRY_SECRET_NAME" \
|
|
"${forgejo_args[@]}"
|
|
|
|
if [[ "$SYNC_FORGEJO_REMOTE" == "1" ]]; then
|
|
: "${FORGEJO_PUSH_USERNAME:=${FORGEJO_API_USERNAME:-$FORGEJO_REPO_OWNER}}"
|
|
: "${FORGEJO_PUSH_PASSWORD:=${FORGEJO_ADMIN_PASSWORD:-${FORGEJO_API_PASSWORD:-}}}"
|
|
|
|
if [[ -n "${FORGEJO_PUSH_USERNAME:-}" && -n "${FORGEJO_PUSH_PASSWORD:-}" ]]; then
|
|
push_url="$(
|
|
python3 - "$FORGEJO_URL" "$FORGEJO_REPO_OWNER" "$FORGEJO_REPO_NAME" "$FORGEJO_PUSH_USERNAME" "$FORGEJO_PUSH_PASSWORD" <<'PY'
|
|
import sys
|
|
from urllib.parse import quote
|
|
|
|
base_url, owner, repo, username, password = sys.argv[1:]
|
|
print(f"{base_url.rstrip('/')}".replace('://', f'://{quote(username, safe="")}:{quote(password, safe="")}@') + f'/{owner}/{repo}.git')
|
|
PY
|
|
)"
|
|
git -C "$ROOT_DIR" remote set-url "$FORGEJO_REMOTE_NAME" "$push_url"
|
|
echo "updated git remote $FORGEJO_REMOTE_NAME for HTTPS push auth"
|
|
fi
|
|
fi
|
|
|
|
cat <<EOF
|
|
bootstrap complete
|
|
|
|
next:
|
|
git commit -am "..."
|
|
git push $FORGEJO_REMOTE_NAME main
|
|
EOF
|