Update deploy-lab.sh

This commit is contained in:
2026-07-18 03:39:35 +00:00
parent b2c6e28068
commit fc9dfb9374
+195 -45
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Laboratorio estable v4.4.3: ZimaOS + Docker + k3d + Argo CD + Multi-Repo + Governance + Monitoring Recovery + Runner # Laboratorio estable v4.4.6.1: ZimaOS + Docker + k3d + Argo CD + Multi-Repo + Governance + Monitoring + 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.
@@ -10,6 +10,7 @@
# gitops Actualiza el repo y aplica root-apps-registry. # gitops Actualiza el repo y aplica root-apps-registry.
# 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.
# 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.
@@ -52,6 +53,10 @@ 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}" INSTALL_HEADLAMP_RBAC="${INSTALL_HEADLAMP_RBAC:-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_TOKEN_SECRET="${POSTDEPLOY_TOKEN_SECRET:-gitea-postdeploy-validator-token}"
POSTDEPLOY_NAMESPACE="${POSTDEPLOY_NAMESPACE:-ecommerce}"
POSTDEPLOY_TOKEN_WAIT_SECONDS="${POSTDEPLOY_TOKEN_WAIT_SECONDS:-90}"
WAIT_SECONDS="${WAIT_SECONDS:-240}" WAIT_SECONDS="${WAIT_SECONDS:-240}"
ARGOCD_ROLLOUT_TIMEOUT="${ARGOCD_ROLLOUT_TIMEOUT:-420}" ARGOCD_ROLLOUT_TIMEOUT="${ARGOCD_ROLLOUT_TIMEOUT:-420}"
AGENT_RECOVERY_WAIT="${AGENT_RECOVERY_WAIT:-75}" AGENT_RECOVERY_WAIT="${AGENT_RECOVERY_WAIT:-75}"
@@ -869,20 +874,24 @@ apply_postdeploy_rbac() {
log "Aplicando RBAC de solo lectura para validación postdeploy" log "Aplicando RBAC de solo lectura para validación postdeploy"
kubectl create namespace ecommerce --dry-run=client -o yaml | kubectl apply -f - kubectl create namespace "$POSTDEPLOY_NAMESPACE" \
--dry-run=client \
-o yaml |
kubectl apply -f -
cat <<'YAML' | kubectl apply -f - cat <<YAML | kubectl apply -f -
apiVersion: v1 apiVersion: v1
kind: ServiceAccount kind: ServiceAccount
metadata: metadata:
name: gitea-postdeploy-validator name: ${POSTDEPLOY_SERVICE_ACCOUNT}
namespace: ecommerce namespace: ${POSTDEPLOY_NAMESPACE}
automountServiceAccountToken: false
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: Role kind: Role
metadata: metadata:
name: gitea-postdeploy-validator name: ${POSTDEPLOY_SERVICE_ACCOUNT}
namespace: ecommerce namespace: ${POSTDEPLOY_NAMESPACE}
rules: rules:
- apiGroups: ["apps"] - apiGroups: ["apps"]
resources: ["deployments", "replicasets"] resources: ["deployments", "replicasets"]
@@ -894,51 +903,114 @@ rules:
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding kind: RoleBinding
metadata: metadata:
name: gitea-postdeploy-validator name: ${POSTDEPLOY_SERVICE_ACCOUNT}
namespace: ecommerce namespace: ${POSTDEPLOY_NAMESPACE}
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: gitea-postdeploy-validator name: ${POSTDEPLOY_SERVICE_ACCOUNT}
namespace: ecommerce namespace: ${POSTDEPLOY_NAMESPACE}
roleRef: roleRef:
apiGroup: rbac.authorization.k8s.io apiGroup: rbac.authorization.k8s.io
kind: Role kind: Role
name: gitea-postdeploy-validator name: ${POSTDEPLOY_SERVICE_ACCOUNT}
YAML YAML
ensure_postdeploy_token_secret
}
ensure_postdeploy_token_secret() {
local sa_uid secret_sa_uid=""
sa_uid="$(
kubectl get serviceaccount "$POSTDEPLOY_SERVICE_ACCOUNT" \
-n "$POSTDEPLOY_NAMESPACE" \
-o jsonpath='{.metadata.uid}'
)"
[[ -n "$sa_uid" ]] ||
die "No fue posible obtener el UID del ServiceAccount postdeploy."
if kubectl get secret "$POSTDEPLOY_TOKEN_SECRET" \
-n "$POSTDEPLOY_NAMESPACE" >/dev/null 2>&1; then
secret_sa_uid="$(
kubectl get secret "$POSTDEPLOY_TOKEN_SECRET" \
-n "$POSTDEPLOY_NAMESPACE" \
-o jsonpath='{.metadata.annotations.kubernetes\.io/service-account\.uid}' \
2>/dev/null || true
)"
if [[ -n "$secret_sa_uid" && "$secret_sa_uid" != "$sa_uid" ]]; then
warn "El token postdeploy pertenece a un ServiceAccount anterior; recreándolo."
kubectl delete secret "$POSTDEPLOY_TOKEN_SECRET" \
-n "$POSTDEPLOY_NAMESPACE"
fi
fi
if ! kubectl get secret "$POSTDEPLOY_TOKEN_SECRET" \
-n "$POSTDEPLOY_NAMESPACE" >/dev/null 2>&1; then
cat <<YAML | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: ${POSTDEPLOY_TOKEN_SECRET}
namespace: ${POSTDEPLOY_NAMESPACE}
annotations:
kubernetes.io/service-account.name: ${POSTDEPLOY_SERVICE_ACCOUNT}
type: kubernetes.io/service-account-token
YAML
fi
} }
generate_postdeploy_credentials() { generate_postdeploy_credentials() {
[[ "$APPLY_POSTDEPLOY_RBAC" == "true" ]] || return 0 [[ "$APPLY_POSTDEPLOY_RBAC" == "true" ]] || return 0
local secret_dir="${APP_DATA_PATH}/gitea-actions-secrets" local secret_dir="${APP_DATA_PATH}/gitea-actions-secrets"
local owner tmp_dir token ca_b64 server kubeconfig_file output_file local owner tmp_dir token_b64="" token="" ca_b64="" server
local kubeconfig_file output_file deadline
owner="$(id -un)" owner="$(id -un)"
server="https://${LAB_HOST_IP}:${API_PORT}" server="https://${LAB_HOST_IP}:${API_PORT}"
tmp_dir="$(mktemp -d)" tmp_dir="$(mktemp -d)"
kubeconfig_file="${tmp_dir}/postdeploy-validator.kubeconfig" kubeconfig_file="${tmp_dir}/postdeploy-validator.kubeconfig"
# No aplicamos chmod a un glob con archivos históricos que podrían pertenecer
# a root. Generamos todo en un directorio temporal y luego instalamos cada
# archivo con propietario y permisos explícitos.
sudo_cmd install -d -m 0700 -o "$owner" "$secret_dir" sudo_cmd install -d -m 0700 -o "$owner" "$secret_dir"
token="$( ensure_postdeploy_token_secret
kubectl create token gitea-postdeploy-validator \
-n ecommerce \
--duration=8760h
)"
deadline=$((SECONDS + POSTDEPLOY_TOKEN_WAIT_SECONDS))
while (( SECONDS < deadline )); do
token_b64="$(
kubectl get secret "$POSTDEPLOY_TOKEN_SECRET" \
-n "$POSTDEPLOY_NAMESPACE" \
-o jsonpath='{.data.token}' 2>/dev/null || true
)"
ca_b64="$( ca_b64="$(
kubectl config view \ kubectl get secret "$POSTDEPLOY_TOKEN_SECRET" \
--raw \ -n "$POSTDEPLOY_NAMESPACE" \
--minify \ -o jsonpath='{.data.ca\.crt}' 2>/dev/null || true
--flatten \
-o jsonpath='{.clusters[0].cluster.certificate-authority-data}'
)" )"
[[ -n "$token" ]] || die "No fue posible generar el token postdeploy." if [[ -n "$token_b64" && -n "$ca_b64" ]]; then
[[ -n "$ca_b64" ]] || die "No fue posible leer la CA del clúster." break
fi
sleep 2
done
[[ -n "$token_b64" ]] ||
die "El controlador no generó el token en Secret/${POSTDEPLOY_TOKEN_SECRET}."
[[ -n "$ca_b64" ]] ||
die "El controlador no generó ca.crt en Secret/${POSTDEPLOY_TOKEN_SECRET}."
token="$(
printf '%s' "$token_b64" |
base64 --decode
)"
[[ -n "$token" ]] ||
die "El token postdeploy quedó vacío."
printf '%s\n' "$token" > "${tmp_dir}/K8S_TOKEN.txt" printf '%s\n' "$token" > "${tmp_dir}/K8S_TOKEN.txt"
printf '%s\n' "$ca_b64" > "${tmp_dir}/K8S_CA_B64.txt" printf '%s\n' "$ca_b64" > "${tmp_dir}/K8S_CA_B64.txt"
@@ -955,20 +1027,18 @@ clusters:
server: ${server} server: ${server}
tls-server-name: ${LAB_HOST_IP} tls-server-name: ${LAB_HOST_IP}
contexts: contexts:
- name: gitea-postdeploy-validator@${CLUSTER_NAME} - name: ${POSTDEPLOY_SERVICE_ACCOUNT}@${CLUSTER_NAME}
context: context:
cluster: ${CLUSTER_NAME} cluster: ${CLUSTER_NAME}
namespace: ecommerce namespace: ${POSTDEPLOY_NAMESPACE}
user: gitea-postdeploy-validator user: ${POSTDEPLOY_SERVICE_ACCOUNT}
current-context: gitea-postdeploy-validator@${CLUSTER_NAME} current-context: ${POSTDEPLOY_SERVICE_ACCOUNT}@${CLUSTER_NAME}
users: users:
- name: gitea-postdeploy-validator - name: ${POSTDEPLOY_SERVICE_ACCOUNT}
user: user:
token: ${token} token: ${token}
EOF EOF
# Compatibilidad con el workflow histórico que usa un único secret
# KUBE_CONFIG_DATA en Base64, pero ahora con privilegios mínimos.
base64 "$kubeconfig_file" | base64 "$kubeconfig_file" |
tr -d '\n' \ tr -d '\n' \
> "${tmp_dir}/KUBE_CONFIG_DATA.txt" > "${tmp_dir}/KUBE_CONFIG_DATA.txt"
@@ -988,19 +1058,85 @@ EOF
"${secret_dir}/${output_file}" "${secret_dir}/${output_file}"
done done
# Los kubeconfig históricos también contienen credenciales. Ajustamos sudo_cmd install \
# únicamente propietario y permisos; no asumimos ningún grupo llamado devops. -m 0600 \
for output_file in kubeconfig-gitea.yaml lab-cluster.kubeconfig; do -o "$owner" \
if [[ -f "${secret_dir}/${output_file}" ]]; then "$kubeconfig_file" \
sudo_cmd chown "$owner" "${secret_dir}/${output_file}" "${secret_dir}/postdeploy-validator.kubeconfig"
sudo_cmd chmod 0600 "${secret_dir}/${output_file}"
fi
done
rm -rf "$tmp_dir" rm -rf "$tmp_dir"
warn "Credenciales postdeploy regeneradas en ${secret_dir}." warn "Credenciales postdeploy regeneradas en ${secret_dir}."
warn "Actualiza en Gitea K8S_TOKEN/K8S_CA_B64/K8S_SERVER/K8S_TLS_SERVER_NAME o el secret histórico KUBE_CONFIG_DATA." warn "Actualiza en Gitea el secret KUBE_CONFIG_DATA con KUBE_CONFIG_DATA.txt."
ok "Token persistente asociado a Secret/${POSTDEPLOY_TOKEN_SECRET}."
}
validate_postdeploy_credentials() {
local secret_dir="${APP_DATA_PATH}/gitea-actions-secrets"
local expected_server="https://${LAB_HOST_IP}:${API_PORT}"
local server_file="${secret_dir}/K8S_SERVER.txt"
local tls_file="${secret_dir}/K8S_TLS_SERVER_NAME.txt"
local kubeconfig_file="${secret_dir}/postdeploy-validator.kubeconfig"
local actual_server actual_tls kube_server kube_tls can_read
[[ -s "$server_file" ]] ||
die "No existe ${server_file}."
[[ -s "$tls_file" ]] ||
die "No existe ${tls_file}."
[[ -s "$kubeconfig_file" ]] ||
die "No existe ${kubeconfig_file}."
actual_server="$(tr -d '\r\n\t ' < "$server_file")"
actual_tls="$(tr -d '\r\n\t ' < "$tls_file")"
[[ "$actual_server" == "$expected_server" ]] ||
die "K8S_SERVER incorrecto. Actual=${actual_server}; esperado=${expected_server}."
[[ "$actual_tls" == "$LAB_HOST_IP" ]] ||
die "K8S_TLS_SERVER_NAME incorrecto. Actual=${actual_tls}; esperado=${LAB_HOST_IP}."
kube_server="$(
KUBECONFIG="$kubeconfig_file" \
kubectl config view \
--minify \
-o jsonpath='{.clusters[0].cluster.server}' 2>/dev/null || true
)"
kube_tls="$(
KUBECONFIG="$kubeconfig_file" \
kubectl config view \
--minify \
-o jsonpath='{.clusters[0].cluster.tls-server-name}' 2>/dev/null || true
)"
[[ "$kube_server" == "$expected_server" ]] ||
die "El kubeconfig postdeploy apunta a ${kube_server:-vacío}; esperado=${expected_server}."
[[ "$kube_tls" == "$LAB_HOST_IP" ]] ||
die "El kubeconfig postdeploy usa tls-server-name=${kube_tls:-vacío}; esperado=${LAB_HOST_IP}."
can_read="$(
KUBECONFIG="$kubeconfig_file" \
kubectl auth can-i get deployments \
-n "$POSTDEPLOY_NAMESPACE" 2>/dev/null || true
)"
[[ "$can_read" == "yes" ]] ||
die "El ServiceAccount postdeploy no puede leer Deployments en ecommerce."
KUBECONFIG="$kubeconfig_file" \
kubectl get deployments \
-n "$POSTDEPLOY_NAMESPACE" \
--request-timeout=15s \
>/dev/null
ok "Credenciales postdeploy verificadas contra ${expected_server}."
printf 'K8S_SERVER esperado : %s\n' "$expected_server"
printf 'K8S_TLS_SERVER_NAME esperado : %s\n' "$LAB_HOST_IP"
printf 'Archivos para Gitea : %s\n' "$secret_dir"
} }
@@ -2880,6 +3016,20 @@ main() {
MONITORING_FORCE_RECOVERY=true recover_monitoring_operation true MONITORING_FORCE_RECOVERY=true recover_monitoring_operation true
return return
;; ;;
postdeploy-secrets)
require_commands
ensure_docker
validate_data_mount
validate_host_ip
prepare_directories
cluster_exists || die "No existe ${CLUSTER_NAME}."
write_kubeconfig
wait_for_api
apply_postdeploy_rbac
generate_postdeploy_credentials
validate_postdeploy_credentials
return
;;
esac esac
require_commands require_commands
@@ -2906,7 +3056,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, 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, install-autostart o reset."
;; ;;
esac esac