#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # Entrypoint для Ansible Runner контейнера # Выполняет: настройку SSH, vault-пароля, и запуск ansible-playbook # ───────────────────────────────────────────────────────────────────────────── set -euo pipefail # Цвета для вывода RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # No Color log() { echo -e "${BLUE}[runner]${NC} $*"; } ok() { echo -e "${GREEN}[runner]${NC} ✓ $*"; } warn() { echo -e "${YELLOW}[runner]${NC} ⚠ $*"; } err() { echo -e "${RED}[runner]${NC} ✗ $*" >&2; } # ── Баннер ──────────────────────────────────────────────────────────────────── print_banner() { echo -e "${CYAN}" echo "╔══════════════════════════════════════════════════════╗" echo "║ K3S Ansible Runner 🚀 ║" echo "║ Ansible $(ansible --version | head -1 | awk '{print $3}') • Helm $(helm version --short) • kubectl $(kubectl version --client -o json 2>/dev/null | jq -r .clientVersion.gitVersion 2>/dev/null || echo 'n/a') ║" echo "╚══════════════════════════════════════════════════════╝" echo -e "${NC}" } # ── Справка ─────────────────────────────────────────────────────────────────── print_help() { echo -e "${BOLD}Использование:${NC}" echo "" echo " Через Makefile (рекомендуется):" echo " make install — полный стек" echo " make install-k3s — только K3S" echo " make install-kubevip — только kube-vip" echo " make addon-nfs — NFS сервер + CSI Driver" echo " make addon-ingress-nginx — ingress-nginx" echo " make health — диагностика" echo " make upgrade VERSION=v1.30.0+k3s1" echo "" echo " Напрямую через docker run:" echo " docker run --rm -it \\" echo " -v \$(pwd):/ansible \\" echo " -v ~/.ssh:/root/.ssh:ro \\" echo " -e VAULT_PASSWORD=мой-пароль \\" echo " k3s-ansible install" echo "" echo -e "${BOLD}Переменные окружения:${NC}" echo " VAULT_PASSWORD — пароль Ansible Vault" echo " VAULT_PASSWORD_FILE — путь к файлу с паролем (альтернатива)" echo " ANSIBLE_TAGS — запустить только указанные теги" echo " ANSIBLE_SKIP_TAGS — пропустить указанные теги" echo " EXTRA_VARS — дополнительные переменные (-e)" echo " ANSIBLE_VERBOSITY — уровень verbose (0-4, default: 0)" echo "" echo -e "${BOLD}Доступные команды:${NC}" echo " bootstrap — создать пользователя + задеплоить SSH ключ" echo " k8s-user — создать k8s пользователя + разложить SSH ключи" echo " mdadm — найти RAID массив и смонтировать в /storage" echo " chrony — установить chrony + настроить timezone" echo " k3s-certs — установить systemd таймер ротации сертификатов" echo " install — core кластер (K3S + kube-vip + certs)" echo " install-full — core + аддоны по флагам из group_vars/all/addons.yml" echo " install-addons — только аддоны (флаги из addons.yml)" echo " install-k3s — только K3S" echo " install-kubevip — только kube-vip" echo " addon — установить аддон из addons//playbook.yml" echo " nfs-server | csi-nfs | ingress-nginx | cert-manager" echo " istio | prometheus-stack | metrics-server" echo " argocd | longhorn | kubernetes-dashboard" echo " add-node — добавить k3s ноду (мастер или воркер)" echo " remove-node — безопасно удалить k3s ноду" echo " add-etcd-node — добавить etcd ноду в кластер (external режим)" echo " remove-etcd-node — удалить etcd ноду из кластера (external режим)" echo " etcd-backup — создать снимок etcd" echo " etcd-restore — восстановить etcd" echo " etcd-list — список снимков etcd" echo " upgrade — обновить K3S (нужен VERSION=)" echo " uninstall — удалить весь стек" echo " health — диагностика" echo " verify — проверка стека" echo " ping — проверить SSH до всех нод" echo " shell — интерактивный bash" echo " ansible-playbook — прямой вызов ansible-playbook" echo " ansible — прямой вызов ansible" } # ── Настройка SSH ───────────────────────────────────────────────────────────── setup_ssh() { log "Настройка SSH..." # Создаём директорию если не существует mkdir -p /root/.ssh chmod 700 /root/.ssh 2>/dev/null || true # Конфигурация SSH клиента (если директория доступна на запись) if touch /root/.ssh/.runner_write_test 2>/dev/null; then rm -f /root/.ssh/.runner_write_test cat > /root/.ssh/config << 'SSHEOF' Host * StrictHostKeyChecking no UserKnownHostsFile /dev/null ControlMaster auto ControlPath /tmp/ssh-%r@%h:%p ControlPersist 60s ServerAliveInterval 30 ServerAliveCountMax 3 ConnectTimeout 30 SSHEOF chmod 600 /root/.ssh/config 2>/dev/null || true else warn "/root/.ssh примонтирован read-only — пропускаю запись SSH config" fi # Исправляем права на ключи если они примонтированы if ls /root/.ssh/*.pem 2>/dev/null || ls /root/.ssh/id_* 2>/dev/null; then chmod 600 /root/.ssh/id_* 2>/dev/null || true chmod 600 /root/.ssh/*.pem 2>/dev/null || true ok "SSH ключи найдены" else warn "SSH ключи не найдены в /root/.ssh — убедись что они примонтированы" fi } # ── Настройка Vault пароля ──────────────────────────────────────────────────── setup_vault() { local vault_file="/tmp/.vault_pass" if [[ -n "${VAULT_PASSWORD:-}" ]]; then echo "${VAULT_PASSWORD}" > "${vault_file}" chmod 600 "${vault_file}" export ANSIBLE_VAULT_PASSWORD_FILE="${vault_file}" ok "Vault пароль установлен из переменной VAULT_PASSWORD" elif [[ -n "${VAULT_PASSWORD_FILE:-}" ]] && [[ -f "${VAULT_PASSWORD_FILE}" ]]; then export ANSIBLE_VAULT_PASSWORD_FILE="${VAULT_PASSWORD_FILE}" ok "Vault пароль загружен из файла: ${VAULT_PASSWORD_FILE}" elif [[ -f "/ansible/.vault_pass" ]]; then export ANSIBLE_VAULT_PASSWORD_FILE="/ansible/.vault_pass" ok "Vault пароль найден в /ansible/.vault_pass" else warn "Vault пароль не задан. Используй переменную VAULT_PASSWORD или файл .vault_pass" warn "Если vault не используется — это нормально" fi } # ── Сборка аргументов ansible-playbook ─────────────────────────────────────── build_ansible_args() { local args=() # Файл vault-пароля if [[ -n "${ANSIBLE_VAULT_PASSWORD_FILE:-}" ]]; then args+=("--vault-password-file" "${ANSIBLE_VAULT_PASSWORD_FILE}") fi # Уровень verbose local verbosity="${ANSIBLE_VERBOSITY:-0}" if [[ "${verbosity}" -gt 0 ]]; then args+=("-$(printf 'v%.0s' $(seq 1 "${verbosity}"))") fi # Теги if [[ -n "${ANSIBLE_TAGS:-}" ]]; then args+=("--tags" "${ANSIBLE_TAGS}") fi # Пропустить теги if [[ -n "${ANSIBLE_SKIP_TAGS:-}" ]]; then args+=("--skip-tags" "${ANSIBLE_SKIP_TAGS}") fi # Дополнительные переменные if [[ -n "${EXTRA_VARS:-}" ]]; then args+=("-e" "${EXTRA_VARS}") fi echo "${args[@]:-}" } # ── Запуск playbook ─────────────────────────────────────────────────────────── run_playbook() { local playbook="$1" shift local extra_args=("$@") # shellcheck disable=SC2207 local ansible_args=($(build_ansible_args)) log "Запуск: ansible-playbook ${playbook} ${ansible_args[*]} ${extra_args[*]}" echo "" exec ansible-playbook "${playbook}" \ "${ansible_args[@]}" \ "${extra_args[@]}" } # ───────────────────────────────────────────────────────────────────────────── # MAIN # ───────────────────────────────────────────────────────────────────────────── print_banner setup_ssh setup_vault COMMAND="${1:-help}" shift || true case "${COMMAND}" in # ── Bootstrap ───────────────────────────────────────────────────────────── bootstrap) log "Bootstrap нод: создание пользователя + деплой SSH ключа..." exec ansible-playbook playbooks/bootstrap.yml "$@" ;; # ── k8s-user ────────────────────────────────────────────────────────────── k8s-user) log "Создание k8s пользователя и деплой SSH ключей..." run_playbook playbooks/k8s-user.yml "$@" ;; # ── mdadm ───────────────────────────────────────────────────────────────── mdadm) log "Настройка mdadm RAID и монтирование /storage..." run_playbook playbooks/mdadm.yml "$@" ;; # ── chrony ──────────────────────────────────────────────────────────────── chrony) log "Настройка chrony и синхронизации времени..." run_playbook playbooks/site.yml --tags chrony "$@" ;; # ── k3s-certs ───────────────────────────────────────────────────────────── k3s-certs) log "Установка systemd таймера ротации сертификатов K3S..." run_playbook playbooks/k3s-certs.yml "$@" ;; # ── Аддоны ──────────────────────────────────────────────────────────────── addon) ADDON="${1:-}" shift || true if [[ -z "${ADDON}" ]]; then err "Укажи аддон: addon argocd | addon longhorn | addon kubernetes-dashboard" echo "" echo "Доступные аддоны:" ls /ansible/addons/ 2>/dev/null | sed 's/^/ /' || echo " (нет аддонов)" exit 1 fi PLAYBOOK="/ansible/addons/${ADDON}/playbook.yml" if [[ ! -f "${PLAYBOOK}" ]]; then err "Аддон не найден: addons/${ADDON}/playbook.yml" echo "" echo "Доступные аддоны:" ls /ansible/addons/ 2>/dev/null | sed 's/^/ /' || echo " (нет аддонов)" exit 1 fi log "Устанавливаю аддон: ${ADDON}" run_playbook "${PLAYBOOK}" "$@" ;; # ── Core установка ──────────────────────────────────────────────────────── install) log "Разворачиваю K3S core кластер (K3S + kube-vip + сертификаты)..." run_playbook playbooks/site.yml "$@" ;; install-full) log "Полный стек: core + аддоны по addons.yml..." run_playbook playbooks/site.yml "$@" run_playbook playbooks/addons.yml "$@" ;; # ── Аддоны по флагам ────────────────────────────────────────────────────── install-addons) log "Устанавливаю аддоны по флагам из group_vars/all/addons.yml..." run_playbook playbooks/addons.yml "$@" ;; install-k3s) log "Устанавливаю K3S cluster..." run_playbook playbooks/site.yml --tags k3s "$@" ;; install-kubevip) log "Устанавливаю kube-vip..." run_playbook playbooks/site.yml --tags kube_vip "$@" ;; add-node) if [[ -z "${1:-}" ]]; then err "Укажи ноду: add-node " exit 1 fi log "Добавляю ноду ${1} в кластер..." exec ansible-playbook playbooks/add-node.yml -e "node_to_add=${1}" "${@:2}" ;; remove-node) if [[ -z "${1:-}" ]]; then err "Укажи ноду: remove-node " exit 1 fi log "Удаляю ноду ${1} из кластера..." exec ansible-playbook playbooks/remove-node.yml -e "node_to_remove=${1}" "${@:2}" ;; # ── Управление etcd нодами (external режим) ─────────────────────────────── add-etcd-node) if [[ -z "${1:-}" ]]; then err "Укажи ноду: add-etcd-node " echo "" echo " Нода должна быть в [etcd_nodes] в inventory/hosts.ini" exit 1 fi log "Добавляю etcd ноду ${1}..." exec ansible-playbook playbooks/add-etcd-node.yml -e "node_to_add=${1}" "${@:2}" ;; remove-etcd-node) if [[ -z "${1:-}" ]]; then err "Укажи ноду: remove-etcd-node " exit 1 fi log "Удаляю etcd ноду ${1} из кластера..." exec ansible-playbook playbooks/remove-etcd-node.yml -e "node_to_remove=${1}" "${@:2}" ;; etcd-backup) log "Создаю снимок etcd..." exec ansible-playbook playbooks/etcd-backup.yml "$@" ;; etcd-restore) if [[ -z "${1:-}" ]]; then err "Укажи снимок: etcd-restore " exit 1 fi log "Восстанавливаю etcd из ${1}..." exec ansible-playbook playbooks/etcd-restore.yml -e "etcd_restore_snapshot=${1}" "${@:2}" ;; etcd-list) log "Список снимков etcd..." exec ansible-playbook playbooks/etcd-restore.yml --tags list "$@" ;; upgrade) if [[ -z "${VERSION:-}" ]]; then err "Нужна переменная VERSION. Пример: make upgrade VERSION=v1.30.0+k3s1" exit 1 fi log "Обновляю K3S до ${VERSION}..." run_playbook playbooks/upgrade.yml -e "k3s_version=${VERSION}" "$@" ;; uninstall) warn "Удаление всего стека! Данные будут потеряны." run_playbook playbooks/uninstall.yml -e "confirm_uninstall=yes" "$@" ;; health) log "Диагностика кластера..." run_playbook playbooks/healthcheck.yml "$@" ;; verify) log "Проверка полного стека..." run_playbook playbooks/site.yml --tags verify "$@" ;; ping) log "Проверяю SSH доступность всех нод..." # shellcheck disable=SC2207 local ansible_args=($(build_ansible_args)) exec ansible all -m ping "${ansible_args[@]}" "$@" ;; # ── Molecule тестирование ролей ─────────────────────────────────────────── molecule) ROLE="${1:-}" shift || true if [[ -z "${ROLE}" ]]; then err "Укажи роль: molecule k3s | molecule prometheus-stack | molecule istio" echo "" echo " Пример: make molecule-k3s" exit 1 fi if [[ ! -d "/ansible/roles/${ROLE}" ]]; then err "Роль не найдена: /ansible/roles/${ROLE}" exit 1 fi log "Тестирую роль: ${ROLE}" export JUNIT_OUTPUT_DIR="${JUNIT_OUTPUT_DIR:-/tmp/molecule-junit}" export ANSIBLE_CALLBACKS_ENABLED="${ANSIBLE_CALLBACKS_ENABLED:-junit}" mkdir -p "${JUNIT_OUTPUT_DIR}" cd "/ansible/roles/${ROLE}" exec molecule "${@:-test}" ;; # ── Molecule тестирование аддонов ───────────────────────────────────────── molecule-addon) ADDON="${1:-}" shift || true if [[ -z "${ADDON}" ]]; then err "Укажи аддон: molecule-addon technitium-dns | molecule-addon authelia" echo "" echo "Доступные аддоны с molecule тестами:" find /ansible/addons -name "molecule.yml" 2>/dev/null \ | sed 's|/ansible/addons/||; s|/role/molecule.*||' | sort | sed 's/^/ /' exit 1 fi ADDON_MOLECULE_DIR="/ansible/addons/${ADDON}/role" if [[ ! -d "${ADDON_MOLECULE_DIR}/molecule" ]]; then err "Molecule тесты не найдены: ${ADDON_MOLECULE_DIR}/molecule/" exit 1 fi log "Тестирую аддон: ${ADDON}" export JUNIT_OUTPUT_DIR="${JUNIT_OUTPUT_DIR:-/tmp/molecule-junit}" export ANSIBLE_CALLBACKS_ENABLED="${ANSIBLE_CALLBACKS_ENABLED:-junit}" mkdir -p "${JUNIT_OUTPUT_DIR}" cd "${ADDON_MOLECULE_DIR}" exec molecule "${@:-test}" ;; # ── Molecule кластерный тест (3 master + 2 worker) ──────────────────────── molecule-cluster) log "Тестирую topology кластера (3 master + 2 worker)..." export JUNIT_OUTPUT_DIR="${JUNIT_OUTPUT_DIR:-/tmp/molecule-junit}" export ANSIBLE_CALLBACKS_ENABLED="${ANSIBLE_CALLBACKS_ENABLED:-junit}" mkdir -p "${JUNIT_OUTPUT_DIR}" cd /ansible exec molecule test -s cluster "${@}" ;; molecule-lint) log "Запуск линтинга (yamllint + ansible-lint)..." cd /ansible yamllint . ansible-lint ok "Линтинг прошёл" ;; # ── Molecule HTML отчёт ─────────────────────────────────────────────────── molecule-report) XML_DIR="${1:-/tmp/molecule-junit}" OUTPUT="${2:-/tmp/molecule-report.html}" log "Генерирую HTML отчёт из ${XML_DIR}..." python3 /ansible/scripts/molecule-report.py \ --xml-dir "${XML_DIR}" \ --output "${OUTPUT}" ok "Отчёт: ${OUTPUT}" ;; # ── Прямые вызовы ───────────────────────────────────────────────────────── ansible-playbook) exec ansible-playbook "$@" ;; ansible) exec ansible "$@" ;; helm) exec helm "$@" ;; kubectl) exec kubectl "$@" ;; # ── Shell ───────────────────────────────────────────────────────────────── shell|bash|sh) log "Запуск интерактивного shell..." exec /bin/bash "$@" ;; # ── Помощь ──────────────────────────────────────────────────────────────── help|--help|-h) print_help ;; *) err "Неизвестная команда: ${COMMAND}" echo "" print_help exit 1 ;; esac