Update deploy-lab.sh
This commit is contained in:
+731
-50
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Laboratorio estable v4.4.6.1: ZimaOS + Docker + k3d + Argo CD + Multi-Repo + Governance + Monitoring + CI/CD
|
# Laboratorio candidato v4.5.7: ZimaOS + Docker + k3d + Argo CD + Multi-Repo + Governance + Monitoring + Portainer + CI/CD
|
||||||
#
|
#
|
||||||
# Modos:
|
# Modos:
|
||||||
# bootstrap Crea el clúster si no existe; si existe, lo inicia y reconcilia.
|
# bootstrap Crea el clúster si no existe; si existe, lo inicia y reconcilia.
|
||||||
@@ -11,6 +11,9 @@
|
|||||||
# monitoring-diagnose Diagnostica kube-prometheus-stack/Grafana.
|
# monitoring-diagnose Diagnostica kube-prometheus-stack/Grafana.
|
||||||
# monitoring-recover Finaliza una operación obsoleta y resincroniza Grafana.
|
# monitoring-recover Finaliza una operación obsoleta y resincroniza Grafana.
|
||||||
# postdeploy-secrets Regenera y valida credenciales de Gitea Actions.
|
# postdeploy-secrets Regenera y valida credenciales de Gitea Actions.
|
||||||
|
# portainer Valida o recupera el Portainer Edge Agent.
|
||||||
|
# portainer-adopt Captura de forma persistente el Edge Agent actual.
|
||||||
|
# retire-headlamp Elimina residuos de Headlamp cuando ya no existe en Gitea.
|
||||||
# runner Adopta, crea o recupera el Gitea Actions runner.
|
# runner Adopta, crea o recupera el Gitea Actions runner.
|
||||||
# install-autostart Instala servicio/timer systemd, orden de montajes y autorreparación.
|
# install-autostart Instala servicio/timer systemd, orden de montajes y autorreparación.
|
||||||
# reset Reconstrucción limpia; requiere confirmación explícita.
|
# reset Reconstrucción limpia; requiere confirmación explícita.
|
||||||
@@ -51,7 +54,25 @@ ARGOCD_VERSION="${ARGOCD_VERSION:-v3.2.0}"
|
|||||||
ARGOCD_HOST="${ARGOCD_HOST:-argocd.cruzcloud.net}"
|
ARGOCD_HOST="${ARGOCD_HOST:-argocd.cruzcloud.net}"
|
||||||
EXPECTED_HOST_IP="${EXPECTED_HOST_IP:-192.168.68.61}"
|
EXPECTED_HOST_IP="${EXPECTED_HOST_IP:-192.168.68.61}"
|
||||||
ALLOW_IP_CHANGE="${ALLOW_IP_CHANGE:-false}"
|
ALLOW_IP_CHANGE="${ALLOW_IP_CHANGE:-false}"
|
||||||
INSTALL_HEADLAMP_RBAC="${INSTALL_HEADLAMP_RBAC:-true}"
|
# Portainer Edge Agent para administrar Kubernetes.
|
||||||
|
INSTALL_PORTAINER_EDGE_AGENT="${INSTALL_PORTAINER_EDGE_AGENT:-true}"
|
||||||
|
PORTAINER_REQUIRED="${PORTAINER_REQUIRED:-true}"
|
||||||
|
PORTAINER_NAMESPACE="${PORTAINER_NAMESPACE:-portainer}"
|
||||||
|
PORTAINER_AGENT_DEPLOYMENT="${PORTAINER_AGENT_DEPLOYMENT:-portainer-agent}"
|
||||||
|
PORTAINER_AGENT_SERVICE="${PORTAINER_AGENT_SERVICE:-portainer-agent}"
|
||||||
|
PORTAINER_AGENT_SERVICEACCOUNT="${PORTAINER_AGENT_SERVICEACCOUNT:-portainer-sa-clusteradmin}"
|
||||||
|
PORTAINER_AGENT_CLUSTERROLEBINDING="${PORTAINER_AGENT_CLUSTERROLEBINDING:-portainer-crb-clusteradmin}"
|
||||||
|
PORTAINER_AGENT_CONFIGMAP="${PORTAINER_AGENT_CONFIGMAP:-portainer-agent-edge}"
|
||||||
|
PORTAINER_AGENT_SECRET="${PORTAINER_AGENT_SECRET:-portainer-agent-edge-key}"
|
||||||
|
PORTAINER_AGENT_IMAGE="${PORTAINER_AGENT_IMAGE:-portainer/agent:2.43.0}"
|
||||||
|
PORTAINER_SERVER_URL="${PORTAINER_SERVER_URL:-https://192.168.68.61:9444}"
|
||||||
|
PORTAINER_TUNNEL_HOST="${PORTAINER_TUNNEL_HOST:-192.168.68.61}"
|
||||||
|
PORTAINER_TUNNEL_PORT="${PORTAINER_TUNNEL_PORT:-8000}"
|
||||||
|
PORTAINER_EDGE_INSECURE_POLL="${PORTAINER_EDGE_INSECURE_POLL:-1}"
|
||||||
|
PORTAINER_ROLLOUT_TIMEOUT="${PORTAINER_ROLLOUT_TIMEOUT:-180}"
|
||||||
|
PORTAINER_BUNDLE_FILE="${PORTAINER_BUNDLE_FILE:-${APP_DATA_PATH}/secrets/portainer-edge-agent.bundle.json}"
|
||||||
|
PORTAINER_CAPTURE_ON_SUCCESS="${PORTAINER_CAPTURE_ON_SUCCESS:-true}"
|
||||||
|
REMOVE_LEGACY_HEADLAMP="${REMOVE_LEGACY_HEADLAMP:-true}"
|
||||||
APPLY_POSTDEPLOY_RBAC="${APPLY_POSTDEPLOY_RBAC:-true}"
|
APPLY_POSTDEPLOY_RBAC="${APPLY_POSTDEPLOY_RBAC:-true}"
|
||||||
POSTDEPLOY_SERVICE_ACCOUNT="${POSTDEPLOY_SERVICE_ACCOUNT:-gitea-postdeploy-validator}"
|
POSTDEPLOY_SERVICE_ACCOUNT="${POSTDEPLOY_SERVICE_ACCOUNT:-gitea-postdeploy-validator}"
|
||||||
POSTDEPLOY_TOKEN_SECRET="${POSTDEPLOY_TOKEN_SECRET:-gitea-postdeploy-validator-token}"
|
POSTDEPLOY_TOKEN_SECRET="${POSTDEPLOY_TOKEN_SECRET:-gitea-postdeploy-validator-token}"
|
||||||
@@ -63,6 +84,16 @@ AGENT_RECOVERY_WAIT="${AGENT_RECOVERY_WAIT:-75}"
|
|||||||
AUTO_REREGISTER_STALE_AGENTS="${AUTO_REREGISTER_STALE_AGENTS:-true}"
|
AUTO_REREGISTER_STALE_AGENTS="${AUTO_REREGISTER_STALE_AGENTS:-true}"
|
||||||
CHECK_INGRESS_AFTER_BOOT="${CHECK_INGRESS_AFTER_BOOT:-true}"
|
CHECK_INGRESS_AFTER_BOOT="${CHECK_INGRESS_AFTER_BOOT:-true}"
|
||||||
|
|
||||||
|
# Arranque conservador basado en la secuencia previamente confirmada:
|
||||||
|
# Docker restaura k3d y el runner con unless-stopped; systemd solo interviene
|
||||||
|
# después de una ventana amplia si la API sigue caída.
|
||||||
|
K3D_AUTOSTART="${K3D_AUTOSTART:-false}"
|
||||||
|
AUTOSTART_BOOT_GRACE_SECONDS="${AUTOSTART_BOOT_GRACE_SECONDS:-300}"
|
||||||
|
AUTOSTART_DOCKER_RECOVERY_SECONDS="${AUTOSTART_DOCKER_RECOVERY_SECONDS:-180}"
|
||||||
|
AUTOSTART_K3D_START_TIMEOUT="${AUTOSTART_K3D_START_TIMEOUT:-360}"
|
||||||
|
K3D_CONTAINER_RESTART_POLICY="${K3D_CONTAINER_RESTART_POLICY:-unless-stopped}"
|
||||||
|
GITEA_RUNNER_RESTART_POLICY="${GITEA_RUNNER_RESTART_POLICY:-unless-stopped}"
|
||||||
|
|
||||||
# Bootstrap GitOps / App-of-Apps.
|
# Bootstrap GitOps / App-of-Apps.
|
||||||
BOOTSTRAP_ROOT_APP="${BOOTSTRAP_ROOT_APP:-true}"
|
BOOTSTRAP_ROOT_APP="${BOOTSTRAP_ROOT_APP:-true}"
|
||||||
ROOT_APP_NAME="${ROOT_APP_NAME:-root-apps-registry}"
|
ROOT_APP_NAME="${ROOT_APP_NAME:-root-apps-registry}"
|
||||||
@@ -172,7 +203,7 @@ sudo_cmd() {
|
|||||||
|
|
||||||
require_commands() {
|
require_commands() {
|
||||||
local command_name
|
local command_name
|
||||||
for command_name in docker k3d kubectl curl git awk sed grep findmnt ss tar base64 tr; do
|
for command_name in docker k3d kubectl curl git awk sed grep findmnt ss tar base64 tr flock; do
|
||||||
have "$command_name" || die "No se encontró el comando requerido: ${command_name}"
|
have "$command_name" || die "No se encontró el comando requerido: ${command_name}"
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
@@ -241,6 +272,7 @@ prepare_directories() {
|
|||||||
"$APP_DATA_PATH" \
|
"$APP_DATA_PATH" \
|
||||||
"${APP_DATA_PATH}/config" \
|
"${APP_DATA_PATH}/config" \
|
||||||
"${APP_DATA_PATH}/secrets" \
|
"${APP_DATA_PATH}/secrets" \
|
||||||
|
"${APP_DATA_PATH}/run" \
|
||||||
"$BOOTSTRAP_DIR" \
|
"$BOOTSTRAP_DIR" \
|
||||||
"$BACKUP_DIR" \
|
"$BACKUP_DIR" \
|
||||||
"$GITEA_RUNNER_ROOT" \
|
"$GITEA_RUNNER_ROOT" \
|
||||||
@@ -404,7 +436,57 @@ create_cluster() {
|
|||||||
|
|
||||||
start_cluster() {
|
start_cluster() {
|
||||||
log "Iniciando clúster existente ${CLUSTER_NAME}"
|
log "Iniciando clúster existente ${CLUSTER_NAME}"
|
||||||
k3d cluster start "$CLUSTER_NAME" --wait --timeout "${WAIT_SECONDS}s" || true
|
k3d cluster start \
|
||||||
|
"$CLUSTER_NAME" \
|
||||||
|
--wait \
|
||||||
|
--timeout "${AUTOSTART_K3D_START_TIMEOUT}s"
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster_api_ready() {
|
||||||
|
[[ -s "$KUBECONFIG_PATH" ]] || return 1
|
||||||
|
|
||||||
|
KUBECONFIG="$KUBECONFIG_PATH" \
|
||||||
|
kubectl get --raw='/readyz' \
|
||||||
|
--request-timeout='5s' >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_cluster_api_quiet() {
|
||||||
|
local timeout_seconds="$1"
|
||||||
|
local deadline=$((SECONDS + timeout_seconds))
|
||||||
|
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
if cluster_api_ready; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
acquire_ensure_lock() {
|
||||||
|
install -d -m 0750 "${APP_DATA_PATH}/run"
|
||||||
|
exec 9>"${APP_DATA_PATH}/run/k3d-lab-ensure.lock"
|
||||||
|
|
||||||
|
if ! flock -n 9; then
|
||||||
|
warn "Ya existe otra recuperación del laboratorio en ejecución; se omite esta iteración."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_boot_grace() {
|
||||||
|
[[ "$K3D_AUTOSTART" == "true" ]] || return 0
|
||||||
|
|
||||||
|
local uptime_seconds remaining
|
||||||
|
uptime_seconds="$(
|
||||||
|
awk '{print int($1)}' /proc/uptime 2>/dev/null || printf '0\n'
|
||||||
|
)"
|
||||||
|
|
||||||
|
if (( uptime_seconds < AUTOSTART_BOOT_GRACE_SECONDS )); then
|
||||||
|
remaining=$((AUTOSTART_BOOT_GRACE_SECONDS - uptime_seconds))
|
||||||
|
log "Esperando ${remaining}s para que ZimaOS y Docker restauren las aplicaciones"
|
||||||
|
sleep "$remaining"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
set_restart_policy() {
|
set_restart_policy() {
|
||||||
@@ -413,9 +495,60 @@ set_restart_policy() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (( ${#cluster_containers[@]} > 0 )); then
|
if (( ${#cluster_containers[@]} > 0 )); then
|
||||||
docker update --restart=unless-stopped "${cluster_containers[@]}" >/dev/null
|
docker update \
|
||||||
ok "Política Docker unless-stopped aplicada a ${#cluster_containers[@]} contenedores del clúster."
|
--restart="$K3D_CONTAINER_RESTART_POLICY" \
|
||||||
|
"${cluster_containers[@]}" >/dev/null
|
||||||
|
|
||||||
|
ok "Política Docker ${K3D_CONTAINER_RESTART_POLICY} aplicada a ${#cluster_containers[@]} contenedores k3d."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if docker inspect "$GITEA_RUNNER_CONTAINER" >/dev/null 2>&1; then
|
||||||
|
docker update \
|
||||||
|
--restart="$GITEA_RUNNER_RESTART_POLICY" \
|
||||||
|
"$GITEA_RUNNER_CONTAINER" >/dev/null
|
||||||
|
|
||||||
|
ok "Política Docker ${GITEA_RUNNER_RESTART_POLICY} aplicada a ${GITEA_RUNNER_CONTAINER}."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_runner_if_needed() {
|
||||||
|
local runner_running
|
||||||
|
|
||||||
|
docker inspect "$GITEA_RUNNER_CONTAINER" >/dev/null 2>&1 || return 0
|
||||||
|
|
||||||
|
runner_running="$(
|
||||||
|
docker inspect \
|
||||||
|
-f '{{.State.Running}}' \
|
||||||
|
"$GITEA_RUNNER_CONTAINER" 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ "$runner_running" != "true" ]]; then
|
||||||
|
log "Iniciando ${GITEA_RUNNER_CONTAINER}"
|
||||||
|
docker start "$GITEA_RUNNER_CONTAINER" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
recover_cluster_conservatively() {
|
||||||
|
if cluster_api_ready; then
|
||||||
|
ok "La API ya está disponible; no se ejecutará k3d cluster start."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "La API aún no responde; esperando restauración natural de Docker"
|
||||||
|
|
||||||
|
if wait_for_cluster_api_quiet "$AUTOSTART_DOCKER_RECOVERY_SECONDS"; then
|
||||||
|
ok "La API se recuperó mediante las políticas de Docker."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "La API no se recuperó en ${AUTOSTART_DOCKER_RECOVERY_SECONDS}s; se ejecutará un único k3d cluster start."
|
||||||
|
|
||||||
|
if ! start_cluster; then
|
||||||
|
warn "k3d cluster start devolvió error; se validará igualmente la API porque K3s puede continuar iniciando."
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait_for_cluster_api_quiet "$AUTOSTART_K3D_START_TIMEOUT" ||
|
||||||
|
die "La API de Kubernetes no se recuperó después del arranque conservador."
|
||||||
}
|
}
|
||||||
|
|
||||||
write_kubeconfig() {
|
write_kubeconfig() {
|
||||||
@@ -845,28 +978,500 @@ spec:
|
|||||||
YAML
|
YAML
|
||||||
}
|
}
|
||||||
|
|
||||||
install_headlamp_rbac() {
|
|
||||||
[[ "$INSTALL_HEADLAMP_RBAC" == "true" ]] || return 0
|
|
||||||
|
|
||||||
log "Creando RBAC de Headlamp para laboratorio"
|
portainer_fail_or_warn() {
|
||||||
|
local message="$1"
|
||||||
|
|
||||||
kubectl create namespace headlamp --dry-run=client -o yaml | kubectl apply -f -
|
if [[ "$PORTAINER_REQUIRED" == "true" ]]; then
|
||||||
kubectl create serviceaccount headlamp-admin -n headlamp --dry-run=client -o yaml | kubectl apply -f -
|
die "$message"
|
||||||
|
fi
|
||||||
|
|
||||||
cat <<'YAML' | kubectl apply -f -
|
warn "$message"
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
return 0
|
||||||
kind: ClusterRoleBinding
|
}
|
||||||
metadata:
|
|
||||||
name: headlamp-admin-binding
|
validate_portainer_server() {
|
||||||
subjects:
|
[[ "$INSTALL_PORTAINER_EDGE_AGENT" == "true" ]] || return 0
|
||||||
- kind: ServiceAccount
|
|
||||||
name: headlamp-admin
|
log "Validando Portainer Server"
|
||||||
namespace: headlamp
|
|
||||||
roleRef:
|
if ! curl -ksSf \
|
||||||
apiGroup: rbac.authorization.k8s.io
|
--connect-timeout 5 \
|
||||||
kind: ClusterRole
|
--max-time 12 \
|
||||||
name: cluster-admin
|
-o /dev/null \
|
||||||
YAML
|
"${PORTAINER_SERVER_URL%/}/api/status"; then
|
||||||
|
portainer_fail_or_warn \
|
||||||
|
"Portainer API no responde en ${PORTAINER_SERVER_URL%/}/api/status."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# El servidor Portainer está en el propio ZimaOS. Validamos que el túnel
|
||||||
|
# Standard Edge esté escuchando sin abrir sockets desde el contenedor.
|
||||||
|
if [[ "$PORTAINER_TUNNEL_HOST" == "$LAB_HOST_IP" ||
|
||||||
|
"$PORTAINER_TUNNEL_HOST" == "$EXPECTED_HOST_IP" ||
|
||||||
|
"$PORTAINER_TUNNEL_HOST" == "127.0.0.1" ||
|
||||||
|
"$PORTAINER_TUNNEL_HOST" == "localhost" ]]; then
|
||||||
|
if ! ss -ltn |
|
||||||
|
awk -v port=":${PORTAINER_TUNNEL_PORT}" '
|
||||||
|
$1 == "LISTEN" && $4 ~ port "$" {found=1}
|
||||||
|
END {exit(found ? 0 : 1)}
|
||||||
|
'; then
|
||||||
|
portainer_fail_or_warn \
|
||||||
|
"Portainer no escucha el túnel Edge en ${PORTAINER_TUNNEL_HOST}:${PORTAINER_TUNNEL_PORT}."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "Portainer API y túnel Edge disponibles."
|
||||||
|
}
|
||||||
|
|
||||||
|
portainer_resources_exist() {
|
||||||
|
kubectl get deployment "$PORTAINER_AGENT_DEPLOYMENT" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" >/dev/null 2>&1 &&
|
||||||
|
kubectl get configmap "$PORTAINER_AGENT_CONFIGMAP" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" >/dev/null 2>&1 &&
|
||||||
|
kubectl get secret "$PORTAINER_AGENT_SECRET" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_portainer_edge_bundle() {
|
||||||
|
[[ "$INSTALL_PORTAINER_EDGE_AGENT" == "true" ]] || return 0
|
||||||
|
|
||||||
|
have python3 ||
|
||||||
|
die "Se requiere python3 para generar el respaldo declarativo del Portainer Edge Agent."
|
||||||
|
|
||||||
|
portainer_resources_exist ||
|
||||||
|
die "No existe un Portainer Edge Agent completo para adoptar."
|
||||||
|
|
||||||
|
local temp_dir output_tmp
|
||||||
|
temp_dir="$(mktemp -d)"
|
||||||
|
output_tmp="${PORTAINER_BUNDLE_FILE}.tmp"
|
||||||
|
|
||||||
|
kubectl get namespace "$PORTAINER_NAMESPACE" -o json \
|
||||||
|
> "${temp_dir}/00-namespace.json"
|
||||||
|
|
||||||
|
kubectl get serviceaccount "$PORTAINER_AGENT_SERVICEACCOUNT" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" -o json \
|
||||||
|
> "${temp_dir}/10-serviceaccount.json"
|
||||||
|
|
||||||
|
kubectl get clusterrolebinding "$PORTAINER_AGENT_CLUSTERROLEBINDING" \
|
||||||
|
-o json \
|
||||||
|
> "${temp_dir}/20-clusterrolebinding.json"
|
||||||
|
|
||||||
|
kubectl get service "$PORTAINER_AGENT_SERVICE" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" -o json \
|
||||||
|
> "${temp_dir}/30-service.json"
|
||||||
|
|
||||||
|
kubectl get configmap "$PORTAINER_AGENT_CONFIGMAP" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" -o json \
|
||||||
|
> "${temp_dir}/40-configmap.json"
|
||||||
|
|
||||||
|
kubectl get secret "$PORTAINER_AGENT_SECRET" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" -o json \
|
||||||
|
> "${temp_dir}/50-secret.json"
|
||||||
|
|
||||||
|
kubectl get deployment "$PORTAINER_AGENT_DEPLOYMENT" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" -o json \
|
||||||
|
> "${temp_dir}/60-deployment.json"
|
||||||
|
|
||||||
|
python3 - "$temp_dir" "$output_tmp" <<'PY'
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
source = pathlib.Path(sys.argv[1])
|
||||||
|
destination = pathlib.Path(sys.argv[2])
|
||||||
|
|
||||||
|
items = []
|
||||||
|
|
||||||
|
for filename in sorted(source.glob("*.json")):
|
||||||
|
obj = json.loads(filename.read_text(encoding="utf-8"))
|
||||||
|
metadata = obj.get("metadata", {})
|
||||||
|
|
||||||
|
clean_meta = {
|
||||||
|
"name": metadata["name"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.get("namespace"):
|
||||||
|
clean_meta["namespace"] = metadata["namespace"]
|
||||||
|
|
||||||
|
if metadata.get("labels"):
|
||||||
|
clean_meta["labels"] = metadata["labels"]
|
||||||
|
|
||||||
|
annotations = {
|
||||||
|
key: value
|
||||||
|
for key, value in metadata.get("annotations", {}).items()
|
||||||
|
if key not in {
|
||||||
|
"kubectl.kubernetes.io/last-applied-configuration",
|
||||||
|
"deployment.kubernetes.io/revision",
|
||||||
|
"argocd.argoproj.io/tracking-id",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if annotations:
|
||||||
|
clean_meta["annotations"] = annotations
|
||||||
|
|
||||||
|
clean = {
|
||||||
|
"apiVersion": obj["apiVersion"],
|
||||||
|
"kind": obj["kind"],
|
||||||
|
"metadata": clean_meta,
|
||||||
|
}
|
||||||
|
|
||||||
|
kind = obj["kind"]
|
||||||
|
|
||||||
|
if kind == "Namespace":
|
||||||
|
pass
|
||||||
|
elif kind == "Secret":
|
||||||
|
clean["type"] = obj.get("type", "Opaque")
|
||||||
|
if obj.get("data"):
|
||||||
|
clean["data"] = obj["data"]
|
||||||
|
if obj.get("stringData"):
|
||||||
|
clean["stringData"] = obj["stringData"]
|
||||||
|
elif kind == "ConfigMap":
|
||||||
|
if obj.get("data"):
|
||||||
|
clean["data"] = obj["data"]
|
||||||
|
if obj.get("binaryData"):
|
||||||
|
clean["binaryData"] = obj["binaryData"]
|
||||||
|
else:
|
||||||
|
spec = obj.get("spec", {})
|
||||||
|
|
||||||
|
if kind == "Service":
|
||||||
|
for key in (
|
||||||
|
"clusterIP",
|
||||||
|
"clusterIPs",
|
||||||
|
"ipFamilies",
|
||||||
|
"ipFamilyPolicy",
|
||||||
|
"healthCheckNodePort",
|
||||||
|
"allocateLoadBalancerNodePorts",
|
||||||
|
):
|
||||||
|
spec.pop(key, None)
|
||||||
|
|
||||||
|
for port in spec.get("ports", []):
|
||||||
|
port.pop("nodePort", None)
|
||||||
|
|
||||||
|
if kind == "ServiceAccount":
|
||||||
|
spec.pop("secrets", None)
|
||||||
|
|
||||||
|
clean["spec"] = spec
|
||||||
|
|
||||||
|
# ClusterRoleBinding no usa spec.
|
||||||
|
if kind == "ClusterRoleBinding":
|
||||||
|
clean.pop("spec", None)
|
||||||
|
clean["roleRef"] = obj["roleRef"]
|
||||||
|
clean["subjects"] = obj["subjects"]
|
||||||
|
|
||||||
|
if kind == "ServiceAccount":
|
||||||
|
clean.pop("spec", None)
|
||||||
|
if "automountServiceAccountToken" in obj:
|
||||||
|
clean["automountServiceAccountToken"] = obj[
|
||||||
|
"automountServiceAccountToken"
|
||||||
|
]
|
||||||
|
if obj.get("imagePullSecrets"):
|
||||||
|
clean["imagePullSecrets"] = obj["imagePullSecrets"]
|
||||||
|
|
||||||
|
items.append(clean)
|
||||||
|
|
||||||
|
bundle = {
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "List",
|
||||||
|
"items": items,
|
||||||
|
}
|
||||||
|
|
||||||
|
destination.write_text(
|
||||||
|
json.dumps(bundle, indent=2, sort_keys=False) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
|
||||||
|
install -m 0600 "$output_tmp" "$PORTAINER_BUNDLE_FILE"
|
||||||
|
rm -rf "$temp_dir" "$output_tmp"
|
||||||
|
|
||||||
|
ok "Portainer Edge Agent adoptado en ${PORTAINER_BUNDLE_FILE}."
|
||||||
|
warn "El bundle contiene EDGE_KEY; mantenlo con permisos 0600 y fuera de Gitea."
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_portainer_edge_agent() {
|
||||||
|
[[ -s "$PORTAINER_BUNDLE_FILE" ]] ||
|
||||||
|
return 1
|
||||||
|
|
||||||
|
log "Restaurando Portainer Edge Agent desde el bundle persistente"
|
||||||
|
kubectl apply -f "$PORTAINER_BUNDLE_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_portainer_edge_agent() {
|
||||||
|
local edge_id insecure actual_image
|
||||||
|
local secret_keys secret_encoded_bytes
|
||||||
|
local desired_replicas ready_replicas available_replicas
|
||||||
|
local pod_count ready_pod_count
|
||||||
|
local configmap_referenced=false secret_referenced=false
|
||||||
|
|
||||||
|
portainer_resources_exist ||
|
||||||
|
return 1
|
||||||
|
|
||||||
|
edge_id="$(
|
||||||
|
kubectl get configmap "$PORTAINER_AGENT_CONFIGMAP" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-o jsonpath='{.data.EDGE_ID}' 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
insecure="$(
|
||||||
|
kubectl get configmap "$PORTAINER_AGENT_CONFIGMAP" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-o jsonpath='{.data.EDGE_INSECURE_POLL}' 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
actual_image="$(
|
||||||
|
kubectl get deployment "$PORTAINER_AGENT_DEPLOYMENT" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].image}' \
|
||||||
|
2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
secret_keys="$(
|
||||||
|
kubectl get secret "$PORTAINER_AGENT_SECRET" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-o go-template='{{range $key, $value := .data}}{{printf "%s\n" $key}}{{end}}' \
|
||||||
|
2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
secret_encoded_bytes="$(
|
||||||
|
kubectl get secret "$PORTAINER_AGENT_SECRET" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-o go-template='{{range $key, $value := .data}}{{printf "%d\n" (len $value)}}{{end}}' \
|
||||||
|
2>/dev/null |
|
||||||
|
awk '{sum += $1} END {print sum + 0}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
[[ -n "$edge_id" ]] ||
|
||||||
|
die "ConfigMap/${PORTAINER_AGENT_CONFIGMAP} no contiene EDGE_ID."
|
||||||
|
|
||||||
|
[[ -n "$secret_keys" && "${secret_encoded_bytes:-0}" -gt 0 ]] ||
|
||||||
|
die "Secret/${PORTAINER_AGENT_SECRET} no contiene una credencial Edge válida."
|
||||||
|
|
||||||
|
# Portainer puede cambiar entre env, envFrom, secretKeyRef y volúmenes
|
||||||
|
# según la versión del instalador. La validación no depende de una forma
|
||||||
|
# concreta; solo registra si los objetos aparecen referenciados.
|
||||||
|
if kubectl get deployment "$PORTAINER_AGENT_DEPLOYMENT" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" -o json |
|
||||||
|
grep -Fq "\"${PORTAINER_AGENT_CONFIGMAP}\""; then
|
||||||
|
configmap_referenced=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if kubectl get deployment "$PORTAINER_AGENT_DEPLOYMENT" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" -o json |
|
||||||
|
grep -Fq "\"${PORTAINER_AGENT_SECRET}\""; then
|
||||||
|
secret_referenced=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$configmap_referenced" == "true" ]]; then
|
||||||
|
ok "Deployment referencia ConfigMap/${PORTAINER_AGENT_CONFIGMAP}."
|
||||||
|
else
|
||||||
|
warn "No se detectó una referencia textual al ConfigMap; se validará el runtime."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$secret_referenced" == "true" ]]; then
|
||||||
|
ok "Deployment referencia Secret/${PORTAINER_AGENT_SECRET}."
|
||||||
|
else
|
||||||
|
warn "No se detectó una referencia textual al Secret; se validará el runtime."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PORTAINER_EDGE_INSECURE_POLL" == "1" && "$insecure" != "1" ]]; then
|
||||||
|
warn "EDGE_INSECURE_POLL esperado=1; actual=${insecure:-vacío}."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$actual_image" && "$actual_image" != "$PORTAINER_AGENT_IMAGE" ]]; then
|
||||||
|
warn "Imagen Portainer Agent actual=${actual_image}; referencia=${PORTAINER_AGENT_IMAGE}."
|
||||||
|
fi
|
||||||
|
|
||||||
|
kubectl rollout status "deployment/${PORTAINER_AGENT_DEPLOYMENT}" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
--timeout="${PORTAINER_ROLLOUT_TIMEOUT}s"
|
||||||
|
|
||||||
|
desired_replicas="$(
|
||||||
|
kubectl get deployment "$PORTAINER_AGENT_DEPLOYMENT" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-o jsonpath='{.spec.replicas}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
ready_replicas="$(
|
||||||
|
kubectl get deployment "$PORTAINER_AGENT_DEPLOYMENT" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-o jsonpath='{.status.readyReplicas}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
available_replicas="$(
|
||||||
|
kubectl get deployment "$PORTAINER_AGENT_DEPLOYMENT" \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-o jsonpath='{.status.availableReplicas}'
|
||||||
|
)"
|
||||||
|
|
||||||
|
pod_count="$(
|
||||||
|
kubectl get pods \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-l app=portainer-agent \
|
||||||
|
--no-headers 2>/dev/null |
|
||||||
|
wc -l |
|
||||||
|
tr -d ' '
|
||||||
|
)"
|
||||||
|
|
||||||
|
ready_pod_count="$(
|
||||||
|
kubectl get pods \
|
||||||
|
-n "$PORTAINER_NAMESPACE" \
|
||||||
|
-l app=portainer-agent \
|
||||||
|
-o go-template='{{range .items}}{{range .status.conditions}}{{if and (eq .type "Ready") (eq .status "True")}}{{printf "1\n"}}{{end}}{{end}}{{end}}' \
|
||||||
|
2>/dev/null |
|
||||||
|
wc -l |
|
||||||
|
tr -d ' '
|
||||||
|
)"
|
||||||
|
|
||||||
|
[[ "${desired_replicas:-0}" -gt 0 ]] ||
|
||||||
|
die "Deployment/${PORTAINER_AGENT_DEPLOYMENT} tiene réplicas deseadas inválidas."
|
||||||
|
|
||||||
|
[[ "${ready_replicas:-0}" -eq "$desired_replicas" ]] ||
|
||||||
|
die "Portainer Agent Ready=${ready_replicas:-0}; esperado=${desired_replicas}."
|
||||||
|
|
||||||
|
[[ "${available_replicas:-0}" -eq "$desired_replicas" ]] ||
|
||||||
|
die "Portainer Agent Available=${available_replicas:-0}; esperado=${desired_replicas}."
|
||||||
|
|
||||||
|
# Algunos manifiestos cambian las etiquetas. El rollout es la fuente
|
||||||
|
# principal; el conteo por selector se usa como información adicional.
|
||||||
|
if [[ "${pod_count:-0}" -gt 0 ]]; then
|
||||||
|
printf 'Pods Portainer detectados : %s\n' "$pod_count"
|
||||||
|
printf 'Pods Portainer Ready : %s\n' "$ready_pod_count"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ok "Portainer Edge Agent disponible en namespace ${PORTAINER_NAMESPACE}."
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_portainer_edge_agent() {
|
||||||
|
[[ "$INSTALL_PORTAINER_EDGE_AGENT" == "true" ]] || return 0
|
||||||
|
|
||||||
|
validate_portainer_server
|
||||||
|
|
||||||
|
if ! portainer_resources_exist; then
|
||||||
|
if ! restore_portainer_edge_agent; then
|
||||||
|
portainer_fail_or_warn \
|
||||||
|
"No existe Portainer Edge Agent ni bundle persistente. Ejecuta el comando generado por Portainer y luego: $0 portainer-adopt"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! validate_portainer_edge_agent; then
|
||||||
|
portainer_fail_or_warn "Portainer Edge Agent no quedó disponible."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$PORTAINER_CAPTURE_ON_SUCCESS" == "true" &&
|
||||||
|
! -s "$PORTAINER_BUNDLE_FILE" ]]; then
|
||||||
|
capture_portainer_edge_bundle
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
portainer_adopt_mode() {
|
||||||
|
require_commands
|
||||||
|
ensure_docker
|
||||||
|
validate_data_mount
|
||||||
|
validate_host_ip
|
||||||
|
prepare_directories
|
||||||
|
|
||||||
|
cluster_exists ||
|
||||||
|
die "No existe ${CLUSTER_NAME}."
|
||||||
|
|
||||||
|
write_kubeconfig
|
||||||
|
wait_for_api
|
||||||
|
validate_portainer_server
|
||||||
|
validate_portainer_edge_agent
|
||||||
|
capture_portainer_edge_bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
headlamp_git_references() {
|
||||||
|
local found=0 path
|
||||||
|
|
||||||
|
if [[ -d "${APP_REGISTRY_CACHE_DIR}/workloads/headlamp" ]]; then
|
||||||
|
printf '%s\n' "${APP_REGISTRY_CACHE_DIR}/workloads/headlamp"
|
||||||
|
found=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$APP_REGISTRY_CACHE_DIR" ]]; then
|
||||||
|
while IFS= read -r path; do
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
found=1
|
||||||
|
done < <(
|
||||||
|
grep -RIl \
|
||||||
|
--exclude-dir=.git \
|
||||||
|
--include='*.yaml' \
|
||||||
|
--include='*.yml' \
|
||||||
|
-E 'name:[[:space:]]*headlamp-app([[:space:]]|$)' \
|
||||||
|
"$APP_REGISTRY_CACHE_DIR" 2>/dev/null || true
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$PLATFORM_INFRA_CACHE_DIR" ]]; then
|
||||||
|
while IFS= read -r path; do
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
found=1
|
||||||
|
done < <(
|
||||||
|
grep -RIl \
|
||||||
|
--exclude-dir=.git \
|
||||||
|
--include='*.yaml' \
|
||||||
|
--include='*.yml' \
|
||||||
|
-E 'name:[[:space:]]*headlamp-governance([[:space:]]|$)' \
|
||||||
|
"$PLATFORM_INFRA_CACHE_DIR" 2>/dev/null || true
|
||||||
|
)
|
||||||
|
|
||||||
|
while IFS= read -r path; do
|
||||||
|
printf '%s\n' "$path"
|
||||||
|
found=1
|
||||||
|
done < <(
|
||||||
|
find "$PLATFORM_INFRA_CACHE_DIR" \
|
||||||
|
-path '*/.git' -prune -o \
|
||||||
|
-type d -name 'headlamp-governance' -print \
|
||||||
|
2>/dev/null || true
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
return "$found"
|
||||||
|
}
|
||||||
|
|
||||||
|
retire_headlamp_mode() {
|
||||||
|
require_commands
|
||||||
|
ensure_docker
|
||||||
|
validate_data_mount
|
||||||
|
validate_host_ip
|
||||||
|
prepare_directories
|
||||||
|
|
||||||
|
cluster_exists ||
|
||||||
|
die "No existe ${CLUSTER_NAME}."
|
||||||
|
|
||||||
|
write_kubeconfig
|
||||||
|
wait_for_api
|
||||||
|
sync_gitops_repositories
|
||||||
|
|
||||||
|
local references
|
||||||
|
references="$(headlamp_git_references || true)"
|
||||||
|
|
||||||
|
if [[ -n "$references" ]]; then
|
||||||
|
printf '%s\n' "$references" >&2
|
||||||
|
die "Headlamp todavía está declarado en Gitea. Elimina esas rutas, haz commit/push y repite retire-headlamp."
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Retirando residuos de Headlamp"
|
||||||
|
|
||||||
|
kubectl delete application \
|
||||||
|
headlamp-app \
|
||||||
|
headlamp-governance \
|
||||||
|
-n argocd \
|
||||||
|
--ignore-not-found=true \
|
||||||
|
--wait=false
|
||||||
|
|
||||||
|
kubectl delete clusterrolebinding \
|
||||||
|
headlamp-admin-binding \
|
||||||
|
--ignore-not-found=true
|
||||||
|
|
||||||
|
kubectl delete namespace headlamp \
|
||||||
|
--ignore-not-found=true \
|
||||||
|
--wait=false
|
||||||
|
|
||||||
|
ok "Headlamp retirado. Portainer queda como consola Kubernetes."
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_postdeploy_rbac() {
|
apply_postdeploy_rbac() {
|
||||||
@@ -2541,7 +3146,9 @@ print_credentials() {
|
|||||||
printf ' NPM Argo CD : http://%s:%s\n' "$LAB_HOST_IP" "$HTTP_PORT"
|
printf ' NPM Argo CD : http://%s:%s\n' "$LAB_HOST_IP" "$HTTP_PORT"
|
||||||
printf ' Host Argo CD : %s\n' "$ARGOCD_HOST"
|
printf ' Host Argo CD : %s\n' "$ARGOCD_HOST"
|
||||||
printf ' ArgoCD admin : %s\n' "$argocd_password"
|
printf ' ArgoCD admin : %s\n' "$argocd_password"
|
||||||
printf ' Token Headlamp : kubectl create token headlamp-admin -n headlamp\n'
|
printf ' Portainer API : %s\n' "$PORTAINER_SERVER_URL"
|
||||||
|
printf ' Portainer Edge : deployment/%s namespace/%s\n' \
|
||||||
|
"$PORTAINER_AGENT_DEPLOYMENT" "$PORTAINER_NAMESPACE"
|
||||||
printf ' Kubeconfig : %s\n' "$KUBECONFIG_PATH"
|
printf ' Kubeconfig : %s\n' "$KUBECONFIG_PATH"
|
||||||
printf ' Root Application: %s -> %s/%s@%s\n' \
|
printf ' Root Application: %s -> %s/%s@%s\n' \
|
||||||
"$ROOT_APP_NAME" "$ROOT_APP_REPO_URL" "$ROOT_APP_REPO_PATH" "$ROOT_APP_REVISION"
|
"$ROOT_APP_NAME" "$ROOT_APP_REPO_URL" "$ROOT_APP_REPO_PATH" "$ROOT_APP_REVISION"
|
||||||
@@ -2596,6 +3203,16 @@ status_report() {
|
|||||||
else
|
else
|
||||||
warn "No existe ${GITEA_RUNNER_CONTAINER}."
|
warn "No existe ${GITEA_RUNNER_CONTAINER}."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
log "Portainer"
|
||||||
|
printf 'Server: %s\n' "$PORTAINER_SERVER_URL"
|
||||||
|
printf 'Tunnel: %s:%s\n' "$PORTAINER_TUNNEL_HOST" "$PORTAINER_TUNNEL_PORT"
|
||||||
|
|
||||||
|
if cluster_exists && kubectl get namespace "$PORTAINER_NAMESPACE" >/dev/null 2>&1; then
|
||||||
|
kubectl get deployment,service,configmap,secret -n "$PORTAINER_NAMESPACE" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
warn "No existe el namespace ${PORTAINER_NAMESPACE}."
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
backup_cluster_state() {
|
backup_cluster_state() {
|
||||||
@@ -2646,6 +3263,8 @@ ensure_cluster_only() {
|
|||||||
ensure_docker
|
ensure_docker
|
||||||
validate_data_mount
|
validate_data_mount
|
||||||
validate_host_ip
|
validate_host_ip
|
||||||
|
acquire_ensure_lock
|
||||||
|
wait_for_boot_grace
|
||||||
|
|
||||||
if ! cluster_exists; then
|
if ! cluster_exists; then
|
||||||
if cluster_containers_exist; then
|
if cluster_containers_exist; then
|
||||||
@@ -2654,8 +3273,10 @@ ensure_cluster_only() {
|
|||||||
die "El clúster ${CLUSTER_NAME} no existe. Ejecuta bootstrap manualmente."
|
die "El clúster ${CLUSTER_NAME} no existe. Ejecuta bootstrap manualmente."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
start_cluster
|
|
||||||
set_restart_policy
|
set_restart_policy
|
||||||
|
start_runner_if_needed
|
||||||
|
recover_cluster_conservatively
|
||||||
|
|
||||||
write_kubeconfig
|
write_kubeconfig
|
||||||
wait_for_api
|
wait_for_api
|
||||||
wait_for_nodes
|
wait_for_nodes
|
||||||
@@ -2663,6 +3284,7 @@ ensure_cluster_only() {
|
|||||||
ensure_root_application_present
|
ensure_root_application_present
|
||||||
auto_recover_monitoring_if_stale
|
auto_recover_monitoring_if_stale
|
||||||
ensure_gitea_runner
|
ensure_gitea_runner
|
||||||
|
ensure_portainer_edge_agent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2726,6 +3348,7 @@ install_autostart() {
|
|||||||
local service_user="${SUDO_USER:-devops}"
|
local service_user="${SUDO_USER:-devops}"
|
||||||
local service_home service_group docker_root installed_script
|
local service_home service_group docker_root installed_script
|
||||||
local k3d_path kubectl_path docker_path curl_path tool_path tool_dir
|
local k3d_path kubectl_path docker_path curl_path tool_path tool_dir
|
||||||
|
local service_result service_status
|
||||||
|
|
||||||
service_home="$(getent passwd "$service_user" | cut -d: -f6)"
|
service_home="$(getent passwd "$service_user" | cut -d: -f6)"
|
||||||
service_group="$(id -gn "$service_user")"
|
service_group="$(id -gn "$service_user")"
|
||||||
@@ -2804,19 +3427,22 @@ install_autostart() {
|
|||||||
|
|
||||||
install -d -m 0755 \
|
install -d -m 0755 \
|
||||||
/etc/default \
|
/etc/default \
|
||||||
/etc/systemd/system \
|
/etc/systemd/system
|
||||||
/etc/systemd/system/docker.service.d
|
|
||||||
|
|
||||||
systemctl enable docker.service >/dev/null
|
systemctl enable docker.service >/dev/null
|
||||||
if systemctl list-unit-files containerd.service >/dev/null 2>&1; then
|
if systemctl list-unit-files containerd.service >/dev/null 2>&1; then
|
||||||
systemctl enable containerd.service >/dev/null || true
|
systemctl enable containerd.service >/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > /etc/systemd/system/docker.service.d/10-k3d-storage.conf <<UNIT
|
# v4.5.5: el laboratorio nunca debe modificar las dependencias de
|
||||||
[Unit]
|
# docker.service. Un RequiresMountsFor sobre almacenamiento administrado por
|
||||||
RequiresMountsFor=${APP_DATA_PATH} ${docker_root}
|
# ZimaOS puede impedir el arranque global de Docker y, con ello, de todas las
|
||||||
After=local-fs.target
|
# aplicaciones del NAS. Eliminamos el drop-in creado por versiones previas.
|
||||||
UNIT
|
rm -f /etc/systemd/system/docker.service.d/10-k3d-storage.conf
|
||||||
|
rmdir /etc/systemd/system/docker.service.d 2>/dev/null || true
|
||||||
|
|
||||||
|
set_restart_policy
|
||||||
|
start_runner_if_needed
|
||||||
|
|
||||||
cat > /etc/default/k3d-lab <<ENV
|
cat > /etc/default/k3d-lab <<ENV
|
||||||
CLUSTER_NAME=${CLUSTER_NAME}
|
CLUSTER_NAME=${CLUSTER_NAME}
|
||||||
@@ -2829,6 +3455,11 @@ ARGOCD_ROLLOUT_TIMEOUT=${ARGOCD_ROLLOUT_TIMEOUT}
|
|||||||
AGENT_RECOVERY_WAIT=${AGENT_RECOVERY_WAIT}
|
AGENT_RECOVERY_WAIT=${AGENT_RECOVERY_WAIT}
|
||||||
AUTO_REREGISTER_STALE_AGENTS=${AUTO_REREGISTER_STALE_AGENTS}
|
AUTO_REREGISTER_STALE_AGENTS=${AUTO_REREGISTER_STALE_AGENTS}
|
||||||
CHECK_INGRESS_AFTER_BOOT=${CHECK_INGRESS_AFTER_BOOT}
|
CHECK_INGRESS_AFTER_BOOT=${CHECK_INGRESS_AFTER_BOOT}
|
||||||
|
AUTOSTART_BOOT_GRACE_SECONDS=${AUTOSTART_BOOT_GRACE_SECONDS}
|
||||||
|
AUTOSTART_DOCKER_RECOVERY_SECONDS=${AUTOSTART_DOCKER_RECOVERY_SECONDS}
|
||||||
|
AUTOSTART_K3D_START_TIMEOUT=${AUTOSTART_K3D_START_TIMEOUT}
|
||||||
|
K3D_CONTAINER_RESTART_POLICY=${K3D_CONTAINER_RESTART_POLICY}
|
||||||
|
GITEA_RUNNER_RESTART_POLICY=${GITEA_RUNNER_RESTART_POLICY}
|
||||||
K3D_BIN=${k3d_path}
|
K3D_BIN=${k3d_path}
|
||||||
KUBECTL_BIN=${kubectl_path}
|
KUBECTL_BIN=${kubectl_path}
|
||||||
DOCKER_BIN=${docker_path}
|
DOCKER_BIN=${docker_path}
|
||||||
@@ -2858,6 +3489,24 @@ GITEA_RUNNER_DATA_DIR=${GITEA_RUNNER_DATA_DIR}
|
|||||||
GITEA_RUNNER_IMAGE_FILE=${GITEA_RUNNER_IMAGE_FILE}
|
GITEA_RUNNER_IMAGE_FILE=${GITEA_RUNNER_IMAGE_FILE}
|
||||||
GITEA_RUNNER_TOKEN_FILE=${GITEA_RUNNER_TOKEN_FILE}
|
GITEA_RUNNER_TOKEN_FILE=${GITEA_RUNNER_TOKEN_FILE}
|
||||||
MIGRATE_EXISTING_RUNNER=false
|
MIGRATE_EXISTING_RUNNER=false
|
||||||
|
INSTALL_PORTAINER_EDGE_AGENT=${INSTALL_PORTAINER_EDGE_AGENT}
|
||||||
|
PORTAINER_REQUIRED=${PORTAINER_REQUIRED}
|
||||||
|
PORTAINER_NAMESPACE=${PORTAINER_NAMESPACE}
|
||||||
|
PORTAINER_AGENT_DEPLOYMENT=${PORTAINER_AGENT_DEPLOYMENT}
|
||||||
|
PORTAINER_AGENT_SERVICE=${PORTAINER_AGENT_SERVICE}
|
||||||
|
PORTAINER_AGENT_SERVICEACCOUNT=${PORTAINER_AGENT_SERVICEACCOUNT}
|
||||||
|
PORTAINER_AGENT_CLUSTERROLEBINDING=${PORTAINER_AGENT_CLUSTERROLEBINDING}
|
||||||
|
PORTAINER_AGENT_CONFIGMAP=${PORTAINER_AGENT_CONFIGMAP}
|
||||||
|
PORTAINER_AGENT_SECRET=${PORTAINER_AGENT_SECRET}
|
||||||
|
PORTAINER_AGENT_IMAGE=${PORTAINER_AGENT_IMAGE}
|
||||||
|
PORTAINER_SERVER_URL=${PORTAINER_SERVER_URL}
|
||||||
|
PORTAINER_TUNNEL_HOST=${PORTAINER_TUNNEL_HOST}
|
||||||
|
PORTAINER_TUNNEL_PORT=${PORTAINER_TUNNEL_PORT}
|
||||||
|
PORTAINER_EDGE_INSECURE_POLL=${PORTAINER_EDGE_INSECURE_POLL}
|
||||||
|
PORTAINER_ROLLOUT_TIMEOUT=${PORTAINER_ROLLOUT_TIMEOUT}
|
||||||
|
PORTAINER_BUNDLE_FILE=${PORTAINER_BUNDLE_FILE}
|
||||||
|
PORTAINER_CAPTURE_ON_SUCCESS=${PORTAINER_CAPTURE_ON_SUCCESS}
|
||||||
|
REMOVE_LEGACY_HEADLAMP=${REMOVE_LEGACY_HEADLAMP}
|
||||||
APP_REGISTRY_CACHE_DIR=${APP_REGISTRY_CACHE_DIR}
|
APP_REGISTRY_CACHE_DIR=${APP_REGISTRY_CACHE_DIR}
|
||||||
APP_REGISTRY_ROOT_MANIFEST=${APP_REGISTRY_ROOT_MANIFEST}
|
APP_REGISTRY_ROOT_MANIFEST=${APP_REGISTRY_ROOT_MANIFEST}
|
||||||
APP_REGISTRY_COMMIT_FILE=${APP_REGISTRY_COMMIT_FILE}
|
APP_REGISTRY_COMMIT_FILE=${APP_REGISTRY_COMMIT_FILE}
|
||||||
@@ -2893,7 +3542,8 @@ Description=Recuperar y validar laboratorio k3d
|
|||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
After=local-fs.target network-online.target docker.service
|
After=local-fs.target network-online.target docker.service
|
||||||
Requires=docker.service
|
Requires=docker.service
|
||||||
RequiresMountsFor=${APP_DATA_PATH} ${docker_root}
|
# El propio modo ensure valida /DATA y sale de forma segura si todavía no está montado.
|
||||||
|
# No se agregan dependencias de montaje que puedan bloquear Docker o el arranque global.
|
||||||
StartLimitIntervalSec=15min
|
StartLimitIntervalSec=15min
|
||||||
StartLimitBurst=3
|
StartLimitBurst=3
|
||||||
|
|
||||||
@@ -2906,8 +3556,9 @@ Environment=HOME=${service_home}
|
|||||||
Environment=KUBECONFIG=${KUBECONFIG_PATH}
|
Environment=KUBECONFIG=${KUBECONFIG_PATH}
|
||||||
Environment="PATH=${tool_path}"
|
Environment="PATH=${tool_path}"
|
||||||
EnvironmentFile=-/etc/default/k3d-lab
|
EnvironmentFile=-/etc/default/k3d-lab
|
||||||
|
Environment=K3D_AUTOSTART=true
|
||||||
ExecStart=/bin/bash ${installed_script} ensure
|
ExecStart=/bin/bash ${installed_script} ensure
|
||||||
TimeoutStartSec=900
|
TimeoutStartSec=1500
|
||||||
Restart=no
|
Restart=no
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
@@ -2918,33 +3569,43 @@ UNIT
|
|||||||
Description=Watchdog del laboratorio k3d
|
Description=Watchdog del laboratorio k3d
|
||||||
|
|
||||||
[Timer]
|
[Timer]
|
||||||
OnBootSec=90s
|
# Docker restaura primero k3d y el runner usando unless-stopped. El watchdog
|
||||||
OnUnitInactiveSec=5min
|
# interviene después y solo llama k3d cluster start si la API sigue caída.
|
||||||
|
OnBootSec=5min
|
||||||
|
OnUnitInactiveSec=10min
|
||||||
AccuracySec=20s
|
AccuracySec=20s
|
||||||
Persistent=true
|
Persistent=false
|
||||||
Unit=k3d-lab-ensure.service
|
Unit=k3d-lab-ensure.service
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=timers.target
|
WantedBy=timers.target
|
||||||
UNIT
|
UNIT
|
||||||
|
|
||||||
systemctl daemon-reload
|
# Evitamos que el timer y la prueba inicial intenten arrancar el
|
||||||
systemctl enable --now k3d-lab-ensure.timer
|
# oneshot al mismo tiempo. También limpiamos contadores históricos de
|
||||||
|
# StartLimit antes de probar la unidad recién instalada.
|
||||||
|
systemctl stop k3d-lab-ensure.timer >/dev/null 2>&1 || true
|
||||||
|
systemctl stop k3d-lab-ensure.service >/dev/null 2>&1 || true
|
||||||
|
systemctl reset-failed k3d-lab-ensure.service >/dev/null 2>&1 || true
|
||||||
|
|
||||||
if ! systemctl start k3d-lab-ensure.service; then
|
systemctl daemon-reload
|
||||||
systemctl status k3d-lab-ensure.service --no-pager -l || true
|
systemctl enable --now k3d-lab-ensure.timer >/dev/null
|
||||||
journalctl -u k3d-lab-ensure.service -n 200 --no-pager || true
|
|
||||||
die "La prueba inicial de k3d-lab-ensure.service falló."
|
systemctl is-active --quiet k3d-lab-ensure.timer ||
|
||||||
fi
|
die "El timer k3d-lab-ensure.timer no quedó activo."
|
||||||
|
|
||||||
|
# No ejecutamos inmediatamente el oneshot durante la instalación. Así
|
||||||
|
# evitamos competir con un clúster que todavía se está recuperando.
|
||||||
|
|
||||||
ok "Autostart instalado para ${service_user}."
|
ok "Autostart instalado para ${service_user}."
|
||||||
printf 'Script persistente: %s\n' "$installed_script"
|
printf 'Script persistente: %s\n' "$installed_script"
|
||||||
printf 'k3d para systemd: %s\n' "$k3d_path"
|
printf 'k3d para systemd: %s\n' "$k3d_path"
|
||||||
printf 'kubectl para systemd: %s\n' "$kubectl_path"
|
printf 'kubectl para systemd: %s\n' "$kubectl_path"
|
||||||
printf 'PATH para systemd: %s\n' "$tool_path"
|
printf 'PATH para systemd: %s\n' "$tool_path"
|
||||||
printf 'DockerRootDir protegido por systemd: %s\n' "$docker_root"
|
printf 'DockerRootDir detectado (sin modificar docker.service): %s\n' "$docker_root"
|
||||||
printf 'Logs: journalctl -u k3d-lab-ensure.service -n 200 --no-pager\n'
|
printf 'Logs: journalctl -u k3d-lab-ensure.service -n 200 --no-pager\n'
|
||||||
printf 'Nota: el drop-in de Docker será plenamente verificable después del reinicio de prueba.\n'
|
printf 'Seguridad: docker.service no contiene dependencias del laboratorio.\n'
|
||||||
|
printf 'Orden estable: Docker unless-stopped primero; watchdog después de 5 minutos.\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
@@ -3030,6 +3691,26 @@ main() {
|
|||||||
validate_postdeploy_credentials
|
validate_postdeploy_credentials
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
|
portainer)
|
||||||
|
require_commands
|
||||||
|
ensure_docker
|
||||||
|
validate_data_mount
|
||||||
|
validate_host_ip
|
||||||
|
prepare_directories
|
||||||
|
cluster_exists || die "No existe ${CLUSTER_NAME}."
|
||||||
|
write_kubeconfig
|
||||||
|
wait_for_api
|
||||||
|
ensure_portainer_edge_agent
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
portainer-adopt)
|
||||||
|
portainer_adopt_mode
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
retire-headlamp)
|
||||||
|
retire_headlamp_mode
|
||||||
|
return
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
require_commands
|
require_commands
|
||||||
@@ -3056,7 +3737,7 @@ main() {
|
|||||||
reset_cluster
|
reset_cluster
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
die "Modo no válido: ${MODE}. Usa bootstrap, recover, ensure, status, repo-sync, gitops, runner, monitoring-diagnose, monitoring-recover, postdeploy-secrets, install-autostart o reset."
|
die "Modo no válido: ${MODE}. Usa bootstrap, recover, ensure, status, repo-sync, gitops, runner, monitoring-diagnose, monitoring-recover, postdeploy-secrets, portainer, portainer-adopt, retire-headlamp, install-autostart o reset."
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -3067,7 +3748,6 @@ main() {
|
|||||||
|
|
||||||
install_argocd
|
install_argocd
|
||||||
apply_argocd_ingress
|
apply_argocd_ingress
|
||||||
install_headlamp_rbac
|
|
||||||
apply_postdeploy_rbac
|
apply_postdeploy_rbac
|
||||||
generate_postdeploy_credentials
|
generate_postdeploy_credentials
|
||||||
sync_gitops_repositories
|
sync_gitops_repositories
|
||||||
@@ -3081,6 +3761,7 @@ main() {
|
|||||||
auto_recover_monitoring_if_stale
|
auto_recover_monitoring_if_stale
|
||||||
wait_for_gitops_apps_health
|
wait_for_gitops_apps_health
|
||||||
ensure_gitea_runner
|
ensure_gitea_runner
|
||||||
|
ensure_portainer_edge_agent
|
||||||
validate_runtime_after_boot
|
validate_runtime_after_boot
|
||||||
|
|
||||||
log "Validación final"
|
log "Validación final"
|
||||||
|
|||||||
Reference in New Issue
Block a user