#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=_config.sh source "${SCRIPT_DIR}/_config.sh" log() { printf '[provision] %s\n' "$*"; } die() { printf '[provision][error] %s\n' "$*" >&2; exit 1; } # --- Prerequisites --- command -v hetzner-k3s >/dev/null 2>&1 || die "Missing: hetzner-k3s CLI. Install: https://github.com/vitobotta/hetzner-k3s" command -v kubectl >/dev/null 2>&1 || die "Missing: kubectl" HCLOUD_TOKEN="$(cfg_require cluster.hcloud_token "Hetzner API token")" export HCLOUD_TOKEN # Validate SSH keys SSH_PUB="$(cfg cluster.ssh_public_key | sed "s|~|${HOME}|g")" SSH_PRIV="$(cfg cluster.ssh_private_key | sed "s|~|${HOME}|g")" [[ -f "${SSH_PUB}" ]] || die "SSH public key not found: ${SSH_PUB}" [[ -f "${SSH_PRIV}" ]] || die "SSH private key not found: ${SSH_PRIV}" # --- Generate hetzner-k3s cluster config from config.yaml --- CLUSTER_CONFIG="${DEPLOY_DIR}/cluster-config.yaml" log "Generating cluster-config.yaml from config.yaml..." generate_cluster_config > "${CLUSTER_CONFIG}" # --- Provision --- INSTANCE_TYPE="$(cfg cluster.instance_type)" LOCATION="$(cfg cluster.location)" NODE_COUNT="$(node_count)" log "Provisioning K3s cluster on Hetzner Cloud..." log " Nodes: ${NODE_COUNT}x ${INSTANCE_TYPE} in ${LOCATION}" log " This takes about 5-10 minutes." echo "" hetzner-k3s create --config "${CLUSTER_CONFIG}" KUBECONFIG_PATH="${DEPLOY_DIR}/kubeconfig" if [[ ! -f "${KUBECONFIG_PATH}" ]]; then die "Provisioning completed but kubeconfig not found. Check hetzner-k3s output." fi # --- Write node IPs back to config.yaml --- log "Querying node IPs..." export KUBECONFIG="${KUBECONFIG_PATH}" python3 -c " import yaml, subprocess, json # Get node info from kubectl result = subprocess.run( ['kubectl', 'get', 'nodes', '-o', 'json'], capture_output=True, text=True ) nodes_json = json.loads(result.stdout) # Build name → IP map ip_map = {} for node in nodes_json.get('items', []): name = node['metadata']['name'] for addr in node.get('status', {}).get('addresses', []): if addr['type'] == 'ExternalIP': ip_map[name] = addr['address'] break else: for addr in node.get('status', {}).get('addresses', []): if addr['type'] == 'InternalIP': ip_map[name] = addr['address'] break # Update config.yaml with IPs with open('${CONFIG_FILE}') as f: config = yaml.safe_load(f) updated = 0 for i, node in enumerate(config.get('nodes', [])): for real_name, ip in ip_map.items(): if node['name'] in real_name or real_name in node['name']: config['nodes'][i]['ip'] = ip config['nodes'][i]['name'] = real_name updated += 1 break if updated == 0 and ip_map: # Names didn't match — assign by index for i, (name, ip) in enumerate(sorted(ip_map.items())): if i < len(config['nodes']): config['nodes'][i]['name'] = name config['nodes'][i]['ip'] = ip updated += 1 with open('${CONFIG_FILE}', 'w') as f: yaml.dump(config, f, default_flow_style=False, sort_keys=False) print(f'Updated {updated} node IPs in config.yaml') for name, ip in sorted(ip_map.items()): print(f' {name}: {ip}') " # --- Label Redis node --- REDIS_NODE="$(nodes_with_role redis | head -1)" if [[ -n "${REDIS_NODE}" ]]; then # Find the actual K8s node name that matches ACTUAL_NODE="$(kubectl get nodes -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' | head -1)" log "Labeling node ${ACTUAL_NODE} for Redis..." kubectl label node "${ACTUAL_NODE}" honeydue/redis=true --overwrite fi log "" log "Cluster provisioned successfully." log "" log "Next steps:" log " export KUBECONFIG=${KUBECONFIG_PATH}" log " kubectl get nodes" log " ./scripts/02-setup-secrets.sh"