296 lines
13 KiB
Bash
Executable file
296 lines
13 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR=$(cd "$(dirname "$0")/../.." && pwd)
|
|
# shellcheck disable=SC1091
|
|
source "$ROOT_DIR/scripts/hetzner/lib.sh"
|
|
load_bootstrap_env
|
|
|
|
TF_DIR="$ROOT_DIR/infra/terraform/hetzner"
|
|
STATE_DIR="$ROOT_DIR/.state/hetzner"
|
|
KUBECONFIG_PATH="$STATE_DIR/kubeconfig.yaml"
|
|
CI_KUBECONFIG_PATH="$STATE_DIR/kubeconfig.incluster.yaml"
|
|
OVERLAY_DIR="$ROOT_DIR/deploy/k8s/overlays/hetzner-single-node"
|
|
DEFAULT_PROJECT_NAME="unrip"
|
|
DEFAULT_PROJECT_NAMESPACE="$DEFAULT_PROJECT_NAME"
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
require terraform
|
|
require kubectl
|
|
require curl
|
|
require python3
|
|
require ssh
|
|
require git
|
|
require docker
|
|
require base64
|
|
|
|
resolve_secret_var HCLOUD_TOKEN required
|
|
resolve_secret_var TAILSCALE_AUTH_KEY optional
|
|
resolve_secret_var NEAR_INTENTS_API_KEY required
|
|
resolve_secret_var REGISTRY_PASSWORD required
|
|
resolve_secret_var FORGEJO_ADMIN_PASSWORD required
|
|
resolve_secret_var CLOUDFLARE_API_TOKEN optional
|
|
resolve_secret_var PORKBUN_API_KEY optional
|
|
resolve_secret_var PORKBUN_SECRET_API_KEY optional
|
|
|
|
: "${SSH_PUBLIC_KEY_PATH:?set SSH_PUBLIC_KEY_PATH}"
|
|
: "${PUBLIC_DOMAIN:?set PUBLIC_DOMAIN}"
|
|
: "${LETSENCRYPT_EMAIL:?set LETSENCRYPT_EMAIL}"
|
|
: "${BASE_DOMAIN:?set BASE_DOMAIN}"
|
|
: "${FORGEJO_DOMAIN:=git.${BASE_DOMAIN}}"
|
|
: "${FORGEJO_ROOT_URL:=https://${FORGEJO_DOMAIN}/}"
|
|
: "${REGISTRY_DOMAIN:=registry.${BASE_DOMAIN}}"
|
|
: "${REGISTRY_USERNAME:?set REGISTRY_USERNAME}"
|
|
: "${TF_ADMIN_CIDR_BLOCKS:=}"
|
|
: "${PROJECT_NAME:=$DEFAULT_PROJECT_NAME}"
|
|
: "${PROJECT_NAMESPACE:=$DEFAULT_PROJECT_NAMESPACE}"
|
|
: "${PROJECT_OVERLAY_DIR:=$OVERLAY_DIR}"
|
|
: "${BOOTSTRAP_NODE_NAME:=unrip-1}"
|
|
: "${SKIP_TERRAFORM_APPLY:=0}"
|
|
: "${PROJECT_KUSTOMIZE_PATH:=../../projects/${PROJECT_NAME}/base}"
|
|
: "${PROJECT_SECRET_NAME:=${PROJECT_NAME}-secrets}"
|
|
: "${PROJECT_SECRET_ENV_BASENAME:=${PROJECT_NAME}.env}"
|
|
: "${PROJECT_REGISTRY_SECRET_NAME:=${PROJECT_NAME}-registry-creds}"
|
|
: "${PROJECT_IMAGE_REPOSITORY:=${PROJECT_NAME}}"
|
|
: "${PROJECT_DEPLOYMENTS:=near-intents-ingest dummy-reactor dummy-executor dummy-consumer}"
|
|
: "${FORGEJO_ADMIN_USERNAME:?set FORGEJO_ADMIN_USERNAME}"
|
|
: "${FORGEJO_ADMIN_EMAIL:?set FORGEJO_ADMIN_EMAIL}"
|
|
: "${FORGEJO_RUNNER_NAME:=k3s-runner}"
|
|
: "${FORGEJO_RUNNER_LABELS:=linux-amd64:host}"
|
|
: "${FORGEJO_REPO_OWNER:=$FORGEJO_ADMIN_USERNAME}"
|
|
: "${FORGEJO_REPO_NAME:=$(basename "$ROOT_DIR")}"
|
|
: "${FORGEJO_REPO_PRIVATE:=true}"
|
|
: "${BOOTSTRAP_DELIVERY_MODE:=forgejo-actions}"
|
|
|
|
BOOTSTRAP_IMAGE="${PROJECT_IMAGE_REPOSITORY}:bootstrap"
|
|
PROJECT_SECRET_ENV_PATH="$PROJECT_OVERLAY_DIR/secrets/$PROJECT_SECRET_ENV_BASENAME"
|
|
GENERATED_OVERLAY_DIR="$STATE_DIR/generated-overlay"
|
|
SSH_PUBLIC_KEY=$(cat "$SSH_PUBLIC_KEY_PATH")
|
|
SSH_PRIVATE_KEY_PATH="${SSH_PUBLIC_KEY_PATH%.pub}"
|
|
if [[ ! -f "$SSH_PRIVATE_KEY_PATH" ]]; then
|
|
echo "missing ssh private key for bootstrap: $SSH_PRIVATE_KEY_PATH" >&2
|
|
exit 1
|
|
fi
|
|
|
|
TF_VARS=(
|
|
-var "hcloud_token=$HCLOUD_TOKEN"
|
|
-var "ssh_public_key=$SSH_PUBLIC_KEY"
|
|
-var "public_domain=$PUBLIC_DOMAIN"
|
|
-var "bootstrap_repo_url=local-bootstrap"
|
|
-var "tailscale_auth_key=${TAILSCALE_AUTH_KEY:-}"
|
|
-var "tailscale_control_plane_hostname=${TAILSCALE_CONTROL_PLANE_HOSTNAME:-}"
|
|
)
|
|
if [[ -n "$TF_ADMIN_CIDR_BLOCKS" && "$TF_ADMIN_CIDR_BLOCKS" != '[]' ]]; then
|
|
TF_VARS+=(-var "admin_cidr_blocks=$TF_ADMIN_CIDR_BLOCKS")
|
|
fi
|
|
|
|
if [[ -n "${TAILSCALE_AUTH_KEY:-}" ]]; then
|
|
bash "$ROOT_DIR/scripts/hetzner/print-tailscale-firewall-note.sh"
|
|
fi
|
|
|
|
terraform -chdir="$TF_DIR" init
|
|
if [[ "$SKIP_TERRAFORM_APPLY" != "1" ]]; then
|
|
terraform -chdir="$TF_DIR" apply -auto-approve "${TF_VARS[@]}"
|
|
fi
|
|
|
|
SERVER_IP=$(terraform -chdir="$TF_DIR" output -raw server_ipv4)
|
|
K3S_API_URL=$(terraform -chdir="$TF_DIR" output -raw k3s_api_url)
|
|
if [[ -n "${TAILSCALE_AUTH_KEY:-}" ]]; then
|
|
DISCOVERED_TAILSCALE_HOST="${TAILSCALE_CONTROL_PLANE_HOSTNAME:-$(wait_for_tailscale_node "$BOOTSTRAP_NODE_NAME")}"
|
|
SSH_TARGET="root@${DISCOVERED_TAILSCALE_HOST}"
|
|
K3S_API_URL="https://${DISCOVERED_TAILSCALE_HOST}:6443"
|
|
else
|
|
SSH_TARGET="root@${SERVER_IP}"
|
|
fi
|
|
|
|
if [[ -n "${CLOUDFLARE_API_TOKEN:-}" && -n "${CLOUDFLARE_ZONE_ID:-}" ]]; then
|
|
if ! SERVER_IP="$SERVER_IP" BASE_DOMAIN="$BASE_DOMAIN" bash "$ROOT_DIR/scripts/hetzner/configure-cloudflare-dns.sh"; then
|
|
echo "warning: cloudflare DNS automation failed; continuing without automated DNS" >&2
|
|
fi
|
|
elif [[ -n "${PORKBUN_API_KEY:-}" && -n "${PORKBUN_SECRET_API_KEY:-}" ]]; then
|
|
if ! SERVER_IP="$SERVER_IP" BASE_DOMAIN="$BASE_DOMAIN" bash "$ROOT_DIR/scripts/hetzner/configure-porkbun-dns.sh"; then
|
|
echo "warning: porkbun DNS automation failed; continuing without automated DNS" >&2
|
|
fi
|
|
fi
|
|
|
|
wait_for_ssh "$SSH_TARGET"
|
|
echo "waiting for Kubernetes API on $K3S_API_URL..."
|
|
wait_for_url "${K3S_API_URL}/readyz" "k3s API readiness"
|
|
|
|
ssh -i "$SSH_PRIVATE_KEY_PATH" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SSH_TARGET" 'sudo cat /etc/rancher/k3s/k3s.yaml' \
|
|
| sed "s|https://127.0.0.1:6443|${K3S_API_URL}|" > "$KUBECONFIG_PATH"
|
|
export KUBECONFIG="$KUBECONFIG_PATH"
|
|
|
|
python3 - "$KUBECONFIG_PATH" "$CI_KUBECONFIG_PATH" <<'PY'
|
|
import sys
|
|
import yaml
|
|
src, dst = sys.argv[1], sys.argv[2]
|
|
config = yaml.safe_load(open(src))
|
|
config['clusters'][0]['cluster']['server'] = 'https://kubernetes.default.svc:443'
|
|
yaml.safe_dump(config, open(dst, 'w'), sort_keys=False)
|
|
PY
|
|
|
|
mkdir -p "$PROJECT_OVERLAY_DIR/secrets" "$GENERATED_OVERLAY_DIR"
|
|
cat > "$PROJECT_SECRET_ENV_PATH" <<EOF
|
|
NEAR_INTENTS_API_KEY=$NEAR_INTENTS_API_KEY
|
|
EOF
|
|
cat > "$PROJECT_OVERLAY_DIR/secrets/forgejo.env" <<EOF
|
|
root_url=$FORGEJO_ROOT_URL
|
|
domain=$FORGEJO_DOMAIN
|
|
EOF
|
|
python3 - <<PY
|
|
from pathlib import Path
|
|
|
|
root = Path("$PROJECT_OVERLAY_DIR")
|
|
generated_root = Path("$GENERATED_OVERLAY_DIR")
|
|
project_kustomize_path = "$PROJECT_KUSTOMIZE_PATH"
|
|
project_namespace = "$PROJECT_NAMESPACE"
|
|
project_secret_name = "$PROJECT_SECRET_NAME"
|
|
project_secret_env_basename = "$PROJECT_SECRET_ENV_BASENAME"
|
|
project_overlay_dir = Path("$PROJECT_OVERLAY_DIR").relative_to(Path("$ROOT_DIR"))
|
|
|
|
resources = [f"../../{project_overlay_dir}/../../platform/base"]
|
|
if project_kustomize_path:
|
|
resources.append(f"../../{project_overlay_dir}/{project_kustomize_path}")
|
|
|
|
generated_root.mkdir(parents=True, exist_ok=True)
|
|
(generated_root / "kustomization.yaml").write_text(
|
|
"""apiVersion: kustomize.config.k8s.io/v1beta1
|
|
kind: Kustomization
|
|
resources:
|
|
"""
|
|
+ "".join(f" - {resource}\n" for resource in resources)
|
|
+ """patches:
|
|
- path: ingress-hosts.patch.yaml
|
|
- path: issuer-email.patch.yaml
|
|
- path: storage-class.patch.yaml
|
|
secretGenerator:
|
|
- name: {project_secret_name}
|
|
namespace: {project_namespace}
|
|
envs:
|
|
- ../../{project_overlay_dir}/secrets/{project_secret_env_basename}
|
|
- name: forgejo-secrets
|
|
namespace: forgejo
|
|
envs:
|
|
- ../../{project_overlay_dir}/secrets/forgejo.env
|
|
- name: registry-secrets
|
|
namespace: registry
|
|
files:
|
|
- htpasswd=../../{project_overlay_dir}/secrets/registry.htpasswd
|
|
generatorOptions:
|
|
disableNameSuffixHash: true
|
|
""".format(
|
|
project_secret_name=project_secret_name,
|
|
project_namespace=project_namespace,
|
|
project_overlay_dir=project_overlay_dir,
|
|
project_secret_env_basename=project_secret_env_basename,
|
|
)
|
|
)
|
|
(generated_root / "storage-class.patch.yaml").write_text((root / "storage-class.patch.yaml").read_text())
|
|
(generated_root / "issuer-email.patch.yaml").write_text(f'''apiVersion: cert-manager.io/v1\nkind: ClusterIssuer\nmetadata:\n name: letsencrypt-staging\nspec:\n acme:\n email: {"$LETSENCRYPT_EMAIL"}\n---\napiVersion: cert-manager.io/v1\nkind: ClusterIssuer\nmetadata:\n name: letsencrypt-production\nspec:\n acme:\n email: {"$LETSENCRYPT_EMAIL"}\n''')
|
|
(generated_root / "ingress-hosts.patch.yaml").write_text(f'''apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: forgejo\n namespace: forgejo\nspec:\n tls:\n - hosts:\n - {"$FORGEJO_DOMAIN"}\n secretName: forgejo-tls\n rules:\n - host: {"$FORGEJO_DOMAIN"}\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: registry\n namespace: registry\nspec:\n tls:\n - hosts:\n - {"$REGISTRY_DOMAIN"}\n secretName: registry-tls\n rules:\n - host: {"$REGISTRY_DOMAIN"}\n''')
|
|
PY
|
|
|
|
kubectl apply -f "$ROOT_DIR/deploy/k8s/platform/base/namespace.yaml"
|
|
kubectl create namespace "$PROJECT_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
|
|
kubectl -n registry create secret generic registry-secrets \
|
|
--from-file=htpasswd=<(docker run --rm --entrypoint htpasswd httpd:2 -Bbn "$REGISTRY_USERNAME" "$REGISTRY_PASSWORD") \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
kubectl -n "$PROJECT_NAMESPACE" create secret docker-registry "$PROJECT_REGISTRY_SECRET_NAME" \
|
|
--docker-server="$REGISTRY_DOMAIN" \
|
|
--docker-username="$REGISTRY_USERNAME" \
|
|
--docker-password="$REGISTRY_PASSWORD" \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
kubectl apply -k "$GENERATED_OVERLAY_DIR"
|
|
|
|
kubectl -n forgejo rollout status deployment/forgejo --timeout=300s
|
|
kubectl -n registry rollout status deployment/registry --timeout=300s
|
|
kubectl -n "$PROJECT_NAMESPACE" rollout status deployment/redpanda --timeout=300s
|
|
kubectl -n forgejo wait --for=condition=available deployment/forgejo-runner --timeout=300s || true
|
|
|
|
forgejo_admin_user_b64=$(printf '%s' "$FORGEJO_ADMIN_USERNAME" | base64 | tr -d '\n')
|
|
forgejo_admin_pass_b64=$(printf '%s' "$FORGEJO_ADMIN_PASSWORD" | base64 | tr -d '\n')
|
|
forgejo_admin_email_b64=$(printf '%s' "$FORGEJO_ADMIN_EMAIL" | base64 | tr -d '\n')
|
|
forgejo_runner_name_b64=$(printf '%s' "$FORGEJO_RUNNER_NAME" | base64 | tr -d '\n')
|
|
forgejo_runner_labels_b64=$(printf '%s' "$FORGEJO_RUNNER_LABELS" | base64 | tr -d '\n')
|
|
kubectl -n forgejo exec -i deploy/forgejo -- /bin/bash --noprofile --norc -s <<EOF
|
|
set -euo pipefail
|
|
ADMIN_USERNAME=\$(printf '%s' '$forgejo_admin_user_b64' | base64 -d)
|
|
ADMIN_PASSWORD=\$(printf '%s' '$forgejo_admin_pass_b64' | base64 -d)
|
|
ADMIN_EMAIL=\$(printf '%s' '$forgejo_admin_email_b64' | base64 -d)
|
|
RUNNER_NAME=\$(printf '%s' '$forgejo_runner_name_b64' | base64 -d)
|
|
RUNNER_LABELS=\$(printf '%s' '$forgejo_runner_labels_b64' | base64 -d)
|
|
APP_INI=/data/gitea/conf/app.ini
|
|
if [[ ! -f "$APP_INI" ]]; then
|
|
echo "missing Forgejo config at $APP_INI" >&2
|
|
exit 1
|
|
fi
|
|
if su-exec git /usr/local/bin/forgejo admin user list | awk 'NR>1 {print \$2}' | grep -qx "\$ADMIN_USERNAME"; then
|
|
echo "forgejo admin already exists: \$ADMIN_USERNAME"
|
|
else
|
|
su-exec git /usr/local/bin/forgejo admin user create --config "$APP_INI" --admin --username "\$ADMIN_USERNAME" --password "\$ADMIN_PASSWORD" --email "\$ADMIN_EMAIL" --must-change-password=false
|
|
fi
|
|
if [[ ! -f /data/.runner ]]; then
|
|
RUNNER_TOKEN=\$(su-exec git /usr/local/bin/forgejo --config "$APP_INI" actions generate-runner-token)
|
|
forgejo-runner register --no-interactive --name "\$RUNNER_NAME" --instance "$FORGEJO_ROOT_URL" --token "\$RUNNER_TOKEN" --labels "\$RUNNER_LABELS"
|
|
install -o 1000 -g 1000 -m 600 .runner /data/.runner
|
|
rm -f .runner
|
|
echo "registered forgejo runner config at /data/.runner"
|
|
else
|
|
echo "forgejo runner already configured: /data/.runner"
|
|
fi
|
|
EOF
|
|
kubectl -n forgejo rollout restart deployment/forgejo-runner
|
|
kubectl -n forgejo rollout status deployment/forgejo-runner --timeout=300s
|
|
|
|
if [[ "$BOOTSTRAP_DELIVERY_MODE" == "forgejo-actions" ]]; then
|
|
wait_for_url "$FORGEJO_ROOT_URL" "Forgejo public URL" 180 5
|
|
wait_for_http_status "https://$REGISTRY_DOMAIN/v2/" "registry public URL" '200|401' 180 5
|
|
|
|
forgejo_bootstrap_args=(
|
|
--forgejo-url "$FORGEJO_ROOT_URL"
|
|
--admin-username "$FORGEJO_ADMIN_USERNAME"
|
|
--admin-password "$FORGEJO_ADMIN_PASSWORD"
|
|
--repo-owner "$FORGEJO_REPO_OWNER"
|
|
--repo-name "$FORGEJO_REPO_NAME"
|
|
--kubeconfig "$KUBECONFIG_PATH"
|
|
--registry-username "$REGISTRY_USERNAME"
|
|
--registry-password "$REGISTRY_PASSWORD"
|
|
--registry-host "$REGISTRY_DOMAIN"
|
|
--project-name "$PROJECT_NAME"
|
|
--project-namespace "$PROJECT_NAMESPACE"
|
|
--project-deployments "${PROJECT_DEPLOYMENTS// /,}"
|
|
)
|
|
if [[ "$FORGEJO_REPO_PRIVATE" == "true" ]]; then
|
|
forgejo_bootstrap_args+=(--repo-private)
|
|
fi
|
|
python3 "$ROOT_DIR/scripts/hetzner/forgejo-bootstrap.py" "${forgejo_bootstrap_args[@]}"
|
|
|
|
bash "$ROOT_DIR/scripts/hetzner/seed-forgejo-repo.sh"
|
|
else
|
|
docker build -t "$BOOTSTRAP_IMAGE" "$ROOT_DIR"
|
|
docker save "$BOOTSTRAP_IMAGE" \
|
|
| ssh -i "$SSH_PRIVATE_KEY_PATH" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$SSH_TARGET" 'sudo k3s ctr images import -'
|
|
|
|
for deployment in $PROJECT_DEPLOYMENTS; do
|
|
kubectl -n "$PROJECT_NAMESPACE" set image "deployment/${deployment}" app="$BOOTSTRAP_IMAGE"
|
|
done
|
|
for deployment in $PROJECT_DEPLOYMENTS; do
|
|
kubectl -n "$PROJECT_NAMESPACE" rollout status "deployment/${deployment}" --timeout=180s
|
|
done
|
|
fi
|
|
|
|
echo "bootstrap complete"
|
|
echo "project_name=$PROJECT_NAME"
|
|
echo "project_namespace=$PROJECT_NAMESPACE"
|
|
echo "project_overlay_dir=$PROJECT_OVERLAY_DIR"
|
|
echo "server_ip=$SERVER_IP"
|
|
echo "ssh_target=$SSH_TARGET"
|
|
echo "k3s_api_url=$K3S_API_URL"
|
|
echo "kubeconfig=$KUBECONFIG_PATH"
|
|
echo "ci_kubeconfig=$CI_KUBECONFIG_PATH"
|
|
echo "bootstrap_delivery_mode=$BOOTSTRAP_DELIVERY_MODE"
|
|
echo "forgejo_url=$FORGEJO_ROOT_URL"
|
|
echo "forgejo_repo=${FORGEJO_ROOT_URL%/}/$FORGEJO_REPO_OWNER/$FORGEJO_REPO_NAME"
|
|
echo "registry_url=https://$REGISTRY_DOMAIN"
|
|
echo "dns_provider=${CLOUDFLARE_API_TOKEN:+cloudflare}${PORKBUN_API_KEY:+porkbun}"
|