init
This commit is contained in:
Executable
+184
@@ -0,0 +1,184 @@
|
||||
#!/bin/bash
|
||||
# Перенос инстансов на другой куб (DST, по умолчанию loop-3) целиком.
|
||||
# loopctl k8s move loop0f66acff77e2 loop5ad226321771 ...
|
||||
# На каждый инстанс:
|
||||
# 1. k8s/rebind.sh: секреты на DST, привязка в визоре (куб, провайдер БД, аннотация ingress)
|
||||
# 2. env в визоре через API: DATASOURCE/DATASOURCEREPLICAS берутся из CR на исходном кубе
|
||||
# 3. sync через API, ждём под на DST, проверяем MM_LICENSE, ping/БД/файлы, лицензию, env против исходного CR
|
||||
# 4. прокси на исходном кубе, выключить там ingress, проверить через прокси, replicas 0
|
||||
# 5. DNS CNAME -> DST_DNS, запись в state/proxy-cleanup.txt
|
||||
# ALLOW_ENV_DIFF=1 — не останавливаться, если env в новом CR отличаются от исходного (только показать).
|
||||
# ALLOW_REPLICAS=N — переносить инстанс с replicas=N на источнике (по умолчанию только 1).
|
||||
# ALLOW_DB_MISMATCH=1 — база в CR и в секрете на источнике разные, брать секрет.
|
||||
# PARALLEL=1 — до 10 инстансов одновременно, каждый в свой лог state/batch-logs/<время>/<id>.log.
|
||||
# Без PARALLEL: любая ошибка = стоп всей пачки. До шага 4 исходный инстанс не трогается.
|
||||
set -euo pipefail
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$HERE/../lib/common.sh"
|
||||
API=$VIZOR_API
|
||||
LB=$(dst_lb); [ -n "$LB" ] || { echo "не резолвится $DST_DNS"; exit 1; }
|
||||
|
||||
[ $# -gt 0 ] || { echo "укажи loop id"; exit 1; }
|
||||
confirm "Перенесу на $DST: $*." || exit 1
|
||||
sku() { base64 -d 2>/dev/null | grep -a -o '"sku_short_name":"[a-z]*"' | cut -d'"' -f4; }
|
||||
cr_env() { kubectl --context "$1" -n loop get mattermost "$2" -o json | jq -r '.spec.mattermostEnv[]|"\(.name)=\(.value)"' | grep -v '^MM_LICENSE=' | sort; }
|
||||
|
||||
move_one() {
|
||||
local ID=$1
|
||||
echo; echo "################ $ID"
|
||||
if [ "$(token_ok)" -lt 5 ]; then
|
||||
[ -z "${PARALLEL_CHILD:-}" ] || { echo "токен истёк в параллельном режиме"; return 1; }
|
||||
refresh_token || return 1
|
||||
fi
|
||||
|
||||
local SRC DOMAIN # источник = куб с CR, registry неактуален
|
||||
for SRC in $SOURCES ""; do
|
||||
[ -n "$SRC" ] || { echo "CR $ID не найден ни на одном исходном кубе"; return 1; }
|
||||
DOMAIN=$(kubectl --context "$SRC" -n loop get mattermost "$ID" -o jsonpath='{.spec.ingress.host}' 2>/dev/null) && [ -n "$DOMAIN" ] && break
|
||||
done
|
||||
local REP; REP=$(kubectl --context "$SRC" -n loop get mattermost "$ID" -o jsonpath='{.spec.replicas}')
|
||||
# replicas 0 на источнике ок, если уже перенесён (продолжение после сбоя на прокси/DNS)
|
||||
[ "$REP" = 1 ] || [ "$REP" = "${ALLOW_REPLICAS:-}" ] || { [ "$REP" = 0 ] && kubectl --context "$DST" -n loop get mattermost "$ID" >/dev/null 2>&1; } || { echo "на $SRC replicas=$REP (не 1) — такие переносим отдельно (ALLOW_REPLICAS=$REP)"; return 1; }
|
||||
# лишний ingress на тот же сервис (у foxford ff-api-v4-ingress на /api/v4) перехватит проверку прокси и после replicas 0 отдаст 503
|
||||
local XING; XING=$(kubectl --context "$SRC" -n loop get ingress -o json | jq -r --arg id "$ID" '.items[]|select(.metadata.name!=$id and any(.spec.rules[]?.http.paths[]?; .backend.service.name==$id))|.metadata.name')
|
||||
[ -z "$XING" ] || { echo "на $SRC есть ещё ingress на сервис $ID: $XING — снять до переноса"; return 1; }
|
||||
|
||||
local VST; VST=$(printf "select status from loop_instance where id like '%%%s' and deleted_at is null;\n" "${ID#loop}" | vsql) || return 1
|
||||
[ "$VST" = RUNNING ] || { echo "в визоре статус '${VST:-нет записи}', не RUNNING — пропускаем, разбираем отдельно"; return 1; }
|
||||
|
||||
# реальная база = env в CR, а не секрет: если они расходятся, перенос БД не доведён (так было у ai-wolfs)
|
||||
local HENV HSEC
|
||||
HENV=$(kubectl --context "$SRC" -n loop get mattermost "$ID" -o json | jq -r '.spec.mattermostEnv[]?|select(.name=="MM_SQLSETTINGS_DATASOURCE").value' | sed -E 's#.*@([^/]+)/.*#\1#')
|
||||
HSEC=$(kubectl --context "$SRC" -n loop get secret "$ID-db-credentials" -o json | jq -r '.data.DB_CONNECTION_STRING|@base64d' | sed -E 's#.*@([^/]+)/.*#\1#')
|
||||
[ "$HENV" = "$HSEC" ] || [ -n "${ALLOW_DB_MISMATCH:-}" ] || { echo "в CR на $SRC база $HENV, а в секрете $HSEC — перенос БД не доведён, не трогаю (ALLOW_DB_MISMATCH=1, если база в секрете актуальна)"; return 1; }
|
||||
|
||||
local RESUME=""
|
||||
if kubectl --context "$DST" -n loop get mattermost "$ID" >/dev/null 2>&1; then
|
||||
# CR на $DST от старой попытки, а визор на старом кубе (откат, как ai-wolfs): sync ушёл бы на $SRC
|
||||
local VCL; VCL=$(printf "select ci.name from loop_instance li join cluster_instance ci on ci.id=li.cluster_instance_id where li.id like '%%%s' and li.deleted_at is null;\n" "${ID#loop}" | vsql) || return 1
|
||||
[ "$VCL" = "$DST" ] || { echo "на $DST есть CR, а в визоре куб $VCL — старый CR на $DST удалить (с бэкапом) и запустить заново"; return 1; }
|
||||
RESUME=1; echo "-- уже есть на $DST, продолжаю с проверок"
|
||||
fi
|
||||
|
||||
# 1
|
||||
[ -n "$RESUME" ] || LOOPCTL_YES=1 bash "$HERE/rebind.sh" "$ID" | grep -v -e '^Готово' -e '^Откат визора' || { echo "rebind.sh упал"; return 1; }
|
||||
local VID; VID=$(printf "select id from loop_instance where id like '%%%s' and deleted_at is null;\n" "${ID#loop}" | vsql) || return 1
|
||||
[ -n "$VID" ] || { echo "не нашёл инстанс в визоре"; return 1; }
|
||||
|
||||
if [ -z "$RESUME" ] || [ -n "${ALLOW_DB_MISMATCH:-}" ]; then
|
||||
# 2: env из визора, DATASOURCE* подменяем значениями из исходного CR (с ALLOW_DB_MISMATCH — из секрета, CR смотрит не туда)
|
||||
local SRCENV; SRCENV=$(kubectl --context "$SRC" -n loop get mattermost "$ID" -o json | jq -c '[.spec.mattermostEnv[]|select(.name|test("^MM_SQLSETTINGS_DATASOURCE(REPLICAS)?$"))|{key:.name,value:.value}]')
|
||||
[ -z "${ALLOW_DB_MISMATCH:-}" ] || SRCENV=$(kubectl --context "$SRC" -n loop get secret "$ID-db-credentials" -o json | jq -c '.data.DB_CONNECTION_STRING|@base64d|[{key:"MM_SQLSETTINGS_DATASOURCE",value:.},{key:"MM_SQLSETTINGS_DATASOURCEREPLICAS",value:.}]')
|
||||
[ "$(jq length <<<"$SRCENV")" -ge 1 ] || { echo "в исходном CR нет DATASOURCE"; return 1; }
|
||||
vapi "$API/$VID" | jq -c --argjson src "$SRCENV" \
|
||||
'{environmentProperties: ([.environmentProperties[]|select(.key|test("^MM_SQLSETTINGS_DATASOURCE(REPLICAS)?$")|not)|{key,value}] + $src)}' \
|
||||
| vapi -X PATCH "$API/$VID" -d @- -o /dev/null || { echo "PATCH env в визоре не прошёл (токен? лимиты?)"; return 1; }
|
||||
echo "-- env в визоре обновлены ($(jq -r '[.[].key]|join(", ")' <<<"$SRCENV"))"
|
||||
|
||||
# свой домен со своим сертификатом: визор создаст CR с wildcard-loop.ru-tls, секрет нужен на $DST до sync
|
||||
local TLS; TLS=$(kubectl --context "$SRC" -n loop get mattermost "$ID" -o jsonpath='{.spec.ingress.tlsSecret}')
|
||||
if [ -n "$TLS" ] && [ "$TLS" != wildcard-loop.ru-tls ]; then
|
||||
kubectl --context "$SRC" -n loop get secret "$TLS" -o json \
|
||||
| jq 'del(.metadata.uid,.metadata.resourceVersion,.metadata.creationTimestamp,.metadata.managedFields,.metadata.ownerReferences,.metadata.annotations)' \
|
||||
| kubectl --context "$DST" -n loop apply -f - >/dev/null || { echo "tls-секрет $TLS не скопировался"; return 1; }
|
||||
echo "-- свой сертификат $TLS скопирован"
|
||||
fi
|
||||
|
||||
# 3
|
||||
vapi -X POST "$API/$VID/synchronization" -o /dev/null || { echo "sync не запустился"; return 1; }; echo "-- sync запущен"
|
||||
local i ok=""
|
||||
# probes как на источнике: визор при создании ставит 30/10, большой инстанс (foxford 600/60) не успеет стартовать
|
||||
for i in $(seq 1 30); do kubectl --context "$DST" -n loop get mattermost "$ID" >/dev/null 2>&1 && break; sleep 2; done
|
||||
kubectl --context "$DST" -n loop patch mattermost "$ID" --type merge \
|
||||
-p "$(kubectl --context "$SRC" -n loop get mattermost "$ID" -o json | jq -c '{spec:{probes:.spec.probes,ingress:{tlsSecret:.spec.ingress.tlsSecret}}}')" >/dev/null || { echo "probes/tlsSecret не скопировались"; return 1; }
|
||||
for i in $(seq 1 36); do # до 6 минут
|
||||
sleep 10
|
||||
[ "$(kubectl --context "$DST" -n loop get pods --no-headers 2>/dev/null | grep "^$ID-" | grep -v Terminating | awk '{print $2}' | head -1)" = 1/1 ] && { ok=1; break; }
|
||||
done
|
||||
[ -n "$ok" ] || { echo "под на $DST не поднялся за 6 минут"; return 1; }
|
||||
fi
|
||||
if ! kubectl --context "$DST" -n loop get mattermost "$ID" -o json | jq -e 'any(.spec.mattermostEnv[]?; .name=="MM_LICENSE")' >/dev/null; then
|
||||
echo "-- MM_LICENSE нет, повторный sync"; vapi -X POST "$API/$VID/synchronization" -o /dev/null; sleep 90
|
||||
fi
|
||||
local ST; ST=$(ping_db "$DOMAIN" "$LB"); [ "$ST" = OK/OK/OK ] || { echo "$DST не здоров: $ST"; return 1; }
|
||||
local L3 LS; L3=$(curl -s -m 10 --resolve "$DOMAIN:443:$LB" "https://$DOMAIN/api/v4/license/client?format=old" | jq -r .SkuShortName)
|
||||
LS=$(kubectl --context "$SRC" -n loop get mattermost "$ID" -o json | jq -r '.spec.mattermostEnv[]|select(.name=="MM_LICENSE").value' | sku)
|
||||
if [ -z "$LS" ]; then # в старом CR нет MM_LICENSE (создан до фикса визора) — берём то, что отдаёт исходный сервер
|
||||
local SIP; SIP=$(kubectl --context "$SRC" -n loop get ingress "$ID" -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
LS=$(curl -s -m 10 --resolve "$DOMAIN:443:$SIP" "https://$DOMAIN/api/v4/license/client?format=old" | jq -r .SkuShortName)
|
||||
fi
|
||||
[ "$L3" = "$LS" ] || { echo "лицензия не совпала: $DST=$L3, было=$LS"; return 1; }
|
||||
local D; D=$(diff <(cr_env "$SRC" "$ID") <(cr_env "$DST" "$ID") || true)
|
||||
if [ -n "$D" ]; then
|
||||
echo "env в CR на $DST отличаются от исходного ('<' было на $SRC, '>' стало из визора):"; sed -E 's#://([^:]+):[^@]+@#://\1:***@#g' <<<"$D" | cut -c1-200
|
||||
[ "${ALLOW_ENV_DIFF:-0}" = 1 ] || { echo "стоп: исходный инстанс не тронут, $DST уже запущен параллельно. Если разница ок — перезапусти с ALLOW_ENV_DIFF=1"; return 1; }
|
||||
fi
|
||||
echo "-- $DST ок: $ST, лицензия $L3, env совпадают"
|
||||
|
||||
# 4
|
||||
# на источнике уже 0 (PAUSED: перед переносом статус RUNNING в визоре, после — pause через API): прокси не нужен
|
||||
if [ "$REP" = 0 ]; then
|
||||
echo "-- на $SRC и так replicas 0, прокси не нужен. После переноса: pause в визоре"
|
||||
else
|
||||
bash "$HERE/proxy.sh" "$ID" "$SRC" >/dev/null || { echo "make-proxy упал"; return 1; }
|
||||
local NAME=${DOMAIN%%.*}-proxy; [[ $NAME =~ ^[a-z] ]] || NAME=p$NAME; local PF="$STATE/proxy/$NAME-${SRC//-/}.yaml"
|
||||
local SRCIP; SRCIP=$(kubectl --context "$SRC" -n loop get ingress "$ID" -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
[ -n "$SRCIP" ] || { echo "не нашёл IP ingress на $SRC"; return 1; }
|
||||
kubectl --context "$SRC" apply -f "$PF" >/dev/null || { echo "прокси не применился"; return 1; }
|
||||
kubectl --context "$SRC" -n loop patch mattermost "$ID" --type merge -p '{"spec":{"ingress":{"enabled":false}}}' >/dev/null || return 1
|
||||
sleep 8
|
||||
ST=$(ping_db "$DOMAIN" "$SRCIP")
|
||||
if [ "$ST" != OK/OK/OK ]; then
|
||||
echo "прокси на $SRC не работает ($ST), возвращаю ingress"
|
||||
kubectl --context "$SRC" -n loop patch mattermost "$ID" --type merge -p '{"spec":{"ingress":{"enabled":true}}}' >/dev/null
|
||||
return 1
|
||||
fi
|
||||
kubectl --context "$SRC" -n loop patch mattermost "$ID" --type merge -p '{"spec":{"replicas":0}}' >/dev/null || return 1
|
||||
echo "-- прокси на $SRC работает ($SRCIP), инстанс на $SRC погашен"
|
||||
fi
|
||||
|
||||
# 5
|
||||
local NOTE=""
|
||||
if [[ $DOMAIN == *.loop.ru ]]; then
|
||||
LOOPCTL_YES=1 bash "$HERE/../dns/dns.sh" cname "${DOMAIN%.loop.ru}" "$DST_DNS" | tail -1 || { echo "DNS не сменился, сделай руками: loopctl dns cname ${DOMAIN%.loop.ru} $DST_DNS"; return 1; }
|
||||
else
|
||||
NOTE=" ВНИМАНИЕ: DNS у клиента, прокси снимать только после того, как $DOMAIN смотрит на $DST_DNS"
|
||||
echo "-- DNS у клиента: попросить сменить $DOMAIN на CNAME $DST_DNS (сейчас: $(dig +short "$DOMAIN" | head -1)). До этого работает через прокси"
|
||||
fi
|
||||
until mkdir "$STATE/.batch-lock" 2>/dev/null; do sleep 0.2; done # общие файлы: по одному процессу
|
||||
[ "$REP" = 0 ] || cat >> "$STATE/proxy-cleanup.txt" <<EOF
|
||||
|
||||
$DOMAIN ($ID), $SRC, batch-move
|
||||
CNAME: $(date '+%Y-%m-%d %H:%M')
|
||||
снимать после: $(date -v+1H '+%Y-%m-%d %H:%M') (не раньше)
|
||||
$NOTE
|
||||
снять прокси:
|
||||
сначала: loopctl proxy traffic $SRC (трафика нет), потом: kubectl --context $SRC delete -f $PF
|
||||
EOF
|
||||
rmdir "$STATE/.batch-lock"
|
||||
echo "== $DOMAIN готово"
|
||||
}
|
||||
|
||||
if [ "${PARALLEL:-0}" = 1 ]; then
|
||||
# параллельно, не больше 10: токен обновляем один раз заранее, у каждого инстанса свой лог
|
||||
[ $# -le 10 ] || { echo "параллельно не больше 10 за раз"; exit 1; }
|
||||
rmdir "$STATE/.batch-lock" 2>/dev/null || true
|
||||
if [ "$(token_ok)" -lt 8 ]; then refresh_token || exit 1; fi
|
||||
LOGDIR=$STATE/batch-logs/$(date +%Y%m%d-%H%M%S); mkdir -p "$LOGDIR"
|
||||
# "|| echo" как в последовательном режиме: внутри move_one set -e не срабатывает, ошибки ловятся явными проверками
|
||||
for ID in "$@"; do (PARALLEL_CHILD=1 move_one "$ID" || echo "СТОП на $ID") > "$LOGDIR/$ID.log" 2>&1 & done
|
||||
echo "запущено параллельно: $* (логи: $LOGDIR)"
|
||||
wait
|
||||
echo; for ID in "$@"; do
|
||||
if grep -q "^== .* готово" "$LOGDIR/$ID.log"; then echo "OK $ID $(grep -m1 "$DST ок" "$LOGDIR/$ID.log" | cut -c4-)"
|
||||
else echo "FAIL $ID: $(tail -1 "$LOGDIR/$ID.log")"; fi
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DONE=()
|
||||
for ID in "$@"; do
|
||||
move_one "$ID" || { echo; echo "СТОП на $ID. Перенесены: ${DONE[*]:-нет}. Остальные не трогал."; exit 1; }
|
||||
DONE+=("$ID")
|
||||
done
|
||||
echo; echo "Пачка готова: $*"
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# Сколько запросов прошло через временные прокси на исходном кубе: два снимка счётчиков ingress-контроллеров с паузой.
|
||||
# loopctl proxy traffic loop-2 [300]
|
||||
# 0 за интервал = трафика нет, прокси можно снимать. Счётчик растёт = кто-то ещё ходит по старому адресу (кэш DNS, прибитый IP).
|
||||
# Редкие запросы (webhook раз в сутки) короткий интервал не поймает: для уверенности сравнить снимки за сутки (state/proxy-counters-*).
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/../lib/common.sh"
|
||||
SRC=${1:?исходный куб, например loop-2}; GAP=${2:-300}
|
||||
|
||||
metrics() { # $1 ns, $2 под, $3 порт; не прочитались = стоп (частичный снимок дал бы ложные дельты)
|
||||
kubectl --context "$SRC" get --raw "/api/v1/namespaces/$1/pods/$2:$3/proxy/metrics" || { echo "метрики $1/$2 не прочитались" >&2; return 1; }
|
||||
}
|
||||
snap() { # "<прокси> <счётчик>" по всем подам traefik (:9100) и ingress-nginx (:10254)
|
||||
local ns p m
|
||||
for ns in $(kubectl --context "$SRC" get pods -A -l app.kubernetes.io/name=traefik -o jsonpath='{.items[*].metadata.namespace}' | tr ' ' '\n' | sort -u); do
|
||||
for p in $(kubectl --context "$SRC" -n "$ns" get pods -l app.kubernetes.io/name=traefik --field-selector=status.phase=Running -o name | cut -d/ -f2); do
|
||||
m=$(metrics "$ns" "$p" 9100) || return 1
|
||||
grep '^traefik_service_requests_total' <<<"$m" | grep -- '-proxy-443@' \
|
||||
| sed -E 's/.*service="loop-([^"]+)-proxy-443@kubernetes".* ([0-9.e+]+)$/\1 \2/'
|
||||
done
|
||||
done
|
||||
for ns in $(kubectl --context "$SRC" get pods -A -l app.kubernetes.io/name=ingress-nginx -o jsonpath='{.items[*].metadata.namespace}' | tr ' ' '\n' | sort -u); do
|
||||
for p in $(kubectl --context "$SRC" -n "$ns" get pods -l app.kubernetes.io/name=ingress-nginx --field-selector=status.phase=Running -o name | cut -d/ -f2); do
|
||||
m=$(metrics "$ns" "$p" 10254) || return 1
|
||||
grep '^nginx_ingress_controller_requests{' <<<"$m" | grep -- 'ingress="[a-z0-9-]*-proxy"' \
|
||||
| sed -E 's/.*ingress="([a-z0-9-]+)-proxy".* ([0-9.e+]+)$/\1 \2/'
|
||||
done
|
||||
done
|
||||
return 0
|
||||
}
|
||||
sum() { awk '{t[$1]+=$2} END{for(k in t) printf "%s %d\n",k,t[k]}' | sort; }
|
||||
|
||||
A=$(snap | sum); [ -n "$A" ] || { echo "на $SRC не нашёл прокси в метриках"; exit 1; }
|
||||
F=$STATE/proxy-counters-$SRC-$(date +%Y%m%d-%H%M).txt; echo "$A" > "$F"
|
||||
echo "снимок 1: $(wc -l <<<"$A" | tr -d ' ') прокси, жду $GAP с..."; sleep "$GAP"
|
||||
B=$(snap | sum); [ -n "$B" ] || { echo "второй снимок пустой (поды ingress перезапустились?), повтори"; exit 1; }
|
||||
join -a2 -e0 -o 0,1.2,2.2 <(echo "$A") <(echo "$B") | awk -v g="$GAP" '{d=$3-$2; printf "%8d %s\n", d, $1}' | sort -rn > "$STATE/.delta"
|
||||
echo "запросов за $GAP с (прокси):"; awk '$1>0' "$STATE/.delta"
|
||||
echo "без трафика: $(awk '$1==0' "$STATE/.delta" | wc -l | tr -d ' ') прокси -> $STATE/proxy-zero-$SRC.txt"
|
||||
awk '$1==0{print $2}' "$STATE/.delta" > "$STATE/proxy-zero-$SRC.txt"
|
||||
echo "снимок для сравнения через сутки: $F"
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
# Генерирует манифест временного прокси <исходный куб> -> DST для инстанса и проверяет его dry-run (не применяет).
|
||||
# loopctl proxy make loop084756df3606 loop-2 -> state/proxy/<поддомен>-proxy-loop2.yaml
|
||||
# Контроллер берётся из текущего ingress инстанса: traefik-* (traefik-enterprise-clients, traefik-loop2) или nginx.
|
||||
set -euo pipefail
|
||||
ID=${1:?loop id}
|
||||
SRC=${2:?исходный куб, например loop-2}
|
||||
. "$(dirname "$0")/../lib/common.sh"
|
||||
LB=$(dst_lb); [ -n "$LB" ] || { echo "не резолвится $DST_DNS" >&2; exit 1; }
|
||||
|
||||
ING=$(kubectl --context "$SRC" -n loop get ingress "$ID" -o json)
|
||||
HOST=$(jq -r '.spec.rules[0].host' <<<"$ING")
|
||||
CLASS=$(jq -r '.spec.ingressClassName // .metadata.annotations["kubernetes.io/ingress.class"]' <<<"$ING")
|
||||
TLS=$(jq -r '.spec.tls[0].secretName' <<<"$ING")
|
||||
NAME=${HOST%%.*}-proxy; [[ $NAME =~ ^[a-z] ]] || NAME=p$NAME # имя k8s не может начинаться с цифры (1kk)
|
||||
mkdir -p "$STATE/proxy"
|
||||
OUT=$STATE/proxy/$NAME-${SRC//-/}.yaml
|
||||
echo "$HOST class=$CLASS tls=$TLS -> $OUT"
|
||||
|
||||
backend() { # общий Service + Endpoints на LB целевого куба
|
||||
cat <<EOF
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: $NAME
|
||||
namespace: loop
|
||||
annotations:
|
||||
$1
|
||||
spec:
|
||||
ports:
|
||||
- name: https
|
||||
port: 443
|
||||
targetPort: 443
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: $NAME
|
||||
namespace: loop
|
||||
subsets:
|
||||
- addresses:
|
||||
- ip: $LB
|
||||
ports:
|
||||
- name: https
|
||||
port: 443
|
||||
EOF
|
||||
}
|
||||
|
||||
ingress() { # $1 класс, $2 аннотации
|
||||
cat <<EOF
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: $NAME
|
||||
namespace: loop
|
||||
annotations:
|
||||
$2
|
||||
spec:
|
||||
ingressClassName: $1
|
||||
tls:
|
||||
- hosts:
|
||||
- $HOST
|
||||
secretName: $TLS
|
||||
rules:
|
||||
- host: $HOST
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: $NAME
|
||||
port:
|
||||
number: 443
|
||||
EOF
|
||||
}
|
||||
|
||||
{
|
||||
echo "# Временный прокси $HOST: $SRC -> $DST ($LB). Откат: kubectl --context $SRC delete -f $OUT"
|
||||
case $CLASS in
|
||||
traefik-*)
|
||||
cat <<EOF
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: ServersTransport
|
||||
metadata:
|
||||
name: $NAME
|
||||
namespace: loop
|
||||
spec:
|
||||
serverName: $HOST
|
||||
EOF
|
||||
backend " traefik.ingress.kubernetes.io/service.serversscheme: https
|
||||
traefik.ingress.kubernetes.io/service.serverstransport: loop-$NAME@kubernetescrd"
|
||||
ingress "$CLASS" " {}" ;;
|
||||
nginx)
|
||||
backend " {}"
|
||||
ingress nginx " nginx.ingress.kubernetes.io/backend-protocol: HTTPS
|
||||
nginx.ingress.kubernetes.io/upstream-vhost: $HOST
|
||||
nginx.ingress.kubernetes.io/proxy-ssl-server-name: \"on\"
|
||||
nginx.ingress.kubernetes.io/proxy-ssl-name: $HOST
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: 1000M
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: \"3600\"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: \"3600\"" ;;
|
||||
*) echo "неизвестный класс ingress: $CLASS" >&2; exit 1 ;;
|
||||
esac
|
||||
} > "$OUT"
|
||||
|
||||
kubectl --context "$SRC" apply --dry-run=server -f "$OUT"
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# Первый шаг переноса инстанса на другой куб (DST, по умолчанию loop-3): секреты + перепривязка в визоре.
|
||||
# loopctl k8s rebind loop12a40f770b5a
|
||||
# Источник = куб из SOURCES, где есть CR; провайдер БД в визоре ищется по host:port из секрета.
|
||||
# Не делает: sync, гашение на исходном кубе, DNS (это loopctl k8s move).
|
||||
set -euo pipefail
|
||||
|
||||
ID=${1:?loop id, например loop12a40f770b5a}
|
||||
. "$(dirname "$0")/../lib/common.sh"
|
||||
|
||||
copy_secret() { # $1 имя; копирует с $SRC на $DST без вывода данных
|
||||
kubectl --context "$SRC" -n loop get secret "$1" -o json \
|
||||
| jq '{apiVersion,kind,type,data,metadata:{name:.metadata.name,namespace:"loop"}}' \
|
||||
| kubectl --context "$DST" apply -f - >/dev/null
|
||||
echo " $1 скопирован"
|
||||
}
|
||||
|
||||
hash_secrets() { # $1 context
|
||||
kubectl --context "$1" -n loop get secret "$ID-db-credentials" "$ID-s3-credentials" "$ID-license" -o json \
|
||||
| jq -S '[.items[]|{n:.metadata.name,t:.type,d:.data}]' | shasum | cut -c1-12
|
||||
}
|
||||
|
||||
echo "== $ID"
|
||||
# источник = куб, где есть CR (registry неактуален); домен из ingress CR
|
||||
for SRC in $SOURCES ""; do
|
||||
[ -n "$SRC" ] || { echo "CR $ID не найден ни на одном исходном кубе"; exit 1; }
|
||||
DOMAIN=$(kubectl --context "$SRC" -n loop get mattermost "$ID" -o jsonpath='{.spec.ingress.host}' 2>/dev/null) && [ -n "$DOMAIN" ] && break
|
||||
done
|
||||
SRC_CLUSTER=$(echo "select id from cluster_instance where name='$SRC';" | vsql)
|
||||
DST_CLUSTER=$(echo "select id from cluster_instance where name='$DST';" | vsql)
|
||||
[ -n "$DST_CLUSTER" ] || { echo "в визоре нет кластера $DST"; exit 1; }
|
||||
echo " $DOMAIN $SRC"
|
||||
|
||||
# база в секрете на исходном кубе уже должна смотреть в Selectel
|
||||
DBHOST=$(kubectl --context "$SRC" -n loop get secret "$ID-db-credentials" -o json | jq -r '.data.DB_CONNECTION_STRING|@base64d' | sed -E 's#.*@([^/]+)/.*#\1#')
|
||||
echo " БД в секрете: $DBHOST"
|
||||
case $DBHOST in *.mdb.yandexcloud.net*) echo "секрет ещё смотрит в Yandex, стоп"; exit 1;; esac
|
||||
|
||||
# CR с таким именем на $DST быть не должно
|
||||
if kubectl --context "$DST" -n loop get mattermost "$ID" >/dev/null 2>&1; then echo "CR уже есть на $DST, стоп"; exit 1; fi
|
||||
|
||||
# провайдер БД = ON_PREM-провайдер визора с тем же host:port, что в секрете
|
||||
PROVIDER=$(echo "select id||'|'||name from database_provider where vendor='ON_PREM' and host_name='${DBHOST%:*}' and host_port::text='${DBHOST##*:}';" | vsql)
|
||||
[ "$(grep -c . <<<"$PROVIDER")" = 1 ] || { echo "в визоре не один ON_PREM-провайдер для $DBHOST: '$PROVIDER'"; exit 1; }
|
||||
TARGET=${PROVIDER#*|}; PROVIDER=${PROVIDER%%|*}; echo " провайдер: $TARGET"
|
||||
|
||||
confirm "Скопирую секреты $ID на $DST и перепривяжу инстанс в визоре на $DST." || exit 1
|
||||
echo "-- секреты на $DST"
|
||||
for s in wildcard-loop.ru-tls private.docker.wilix.dev; do # общие, копируем один раз
|
||||
kubectl --context "$DST" -n loop get secret "$s" >/dev/null 2>&1 || copy_secret "$s"
|
||||
done
|
||||
for s in db-credentials s3-credentials license; do copy_secret "$ID-$s"; done
|
||||
H1=$(hash_secrets "$SRC"); H3=$(hash_secrets "$DST")
|
||||
[ "$H1" = "$H3" ] || { echo "секреты не совпали: $H1 / $H3"; exit 1; }
|
||||
echo " совпадают ($H1)"
|
||||
|
||||
# свой сертификат (не wildcard): cluster-issuer оставляем, иначе на новом кубе он перестанет продлеваться
|
||||
OWN_TLS=$(kubectl --context "$SRC" -n loop get mattermost "$ID" -o jsonpath='{.spec.ingress.tlsSecret}')
|
||||
[ "$OWN_TLS" = wildcard-loop.ru-tls ] && OWN_TLS=""
|
||||
echo "-- визор"
|
||||
TAIL=${ID#loop}
|
||||
FOUND=$(printf "select i.id, i.domain, c.name, d.id from loop_instance i join cluster_instance c on c.id=i.cluster_instance_id join database_instance d on d.id=i.database_instance_id where i.id like '%%%s' and i.deleted_at is null;\n" "$TAIL" | vsql)
|
||||
[ "$(printf '%s\n' "$FOUND" | grep -c .)" = 1 ] || { echo "в визоре не один инстанс: $FOUND"; exit 1; }
|
||||
IFS='|' read -r VID VDOMAIN VCLUSTER DBID <<<"$FOUND"
|
||||
echo " $VID $VDOMAIN куб=$VCLUSTER"
|
||||
[ "$VDOMAIN" = "$DOMAIN" ] || { echo "домен в визоре другой: $VDOMAIN"; exit 1; }
|
||||
|
||||
vsql <<SQL
|
||||
begin;
|
||||
update loop_instance set cluster_instance_id='$DST_CLUSTER', synchronization_status='NOT_SYNCHRONIZED' where id='$VID';
|
||||
update database_instance set vendor='ON_PREM',
|
||||
database_provider_id='$PROVIDER'
|
||||
where id='$DBID';
|
||||
-- на целевом кубе traefik; без класса оператор ставит nginx. cluster-issuer убрать: с wildcard он перезапишет wildcard-loop.ru-tls
|
||||
delete from loop_instance_annotation where loop_instance_id='$VID'
|
||||
and (annotation='kubernetes.io/ingress.class' or (annotation='cert-manager.io/cluster-issuer' and '$OWN_TLS'=''));
|
||||
insert into loop_instance_annotation(id, loop_instance_id, annotation, annotation_value)
|
||||
values (gen_random_uuid()::text, '$VID', 'kubernetes.io/ingress.class', '${DST_INGRESS_CLASS:-traefik}');
|
||||
select i.domain, c.name, p.name, d.vendor from loop_instance i
|
||||
join cluster_instance c on c.id=i.cluster_instance_id
|
||||
join database_instance d on d.id=i.database_instance_id
|
||||
join database_provider p on p.id=d.database_provider_id where i.id='$VID';
|
||||
commit;
|
||||
SQL
|
||||
|
||||
echo "Готово. Дальше: loopctl k8s move $ID (env, sync, проверки, прокси, DNS)."
|
||||
echo "Откат визора: update loop_instance set cluster_instance_id='$SRC_CLUSTER' where id='$VID';"
|
||||
Reference in New Issue
Block a user