diff --git a/deploy-lab.sh b/deploy-lab.sh index 62df9f9..d6f80de 100644 --- a/deploy-lab.sh +++ b/deploy-lab.sh @@ -1,5 +1,5 @@ #!/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: # 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. # monitoring-diagnose Diagnostica kube-prometheus-stack/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. # install-autostart Instala servicio/timer systemd, orden de montajes y autorreparación. # 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}" INSTALL_HEADLAMP_RBAC="${INSTALL_HEADLAMP_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}" ARGOCD_ROLLOUT_TIMEOUT="${ARGOCD_ROLLOUT_TIMEOUT:-420}" AGENT_RECOVERY_WAIT="${AGENT_RECOVERY_WAIT:-75}" @@ -869,20 +874,24 @@ apply_postdeploy_rbac() { 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 </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 </dev/null || true + )" + ca_b64="$( + kubectl get secret "$POSTDEPLOY_TOKEN_SECRET" \ + -n "$POSTDEPLOY_NAMESPACE" \ + -o jsonpath='{.data.ca\.crt}' 2>/dev/null || true + )" + + if [[ -n "$token_b64" && -n "$ca_b64" ]]; then + 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="$( - kubectl create token gitea-postdeploy-validator \ - -n ecommerce \ - --duration=8760h + printf '%s' "$token_b64" | + base64 --decode )" - ca_b64="$( - kubectl config view \ - --raw \ - --minify \ - --flatten \ - -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' - )" - - [[ -n "$token" ]] || die "No fue posible generar el token postdeploy." - [[ -n "$ca_b64" ]] || die "No fue posible leer la CA del clúster." + [[ -n "$token" ]] || + die "El token postdeploy quedó vacío." printf '%s\n' "$token" > "${tmp_dir}/K8S_TOKEN.txt" printf '%s\n' "$ca_b64" > "${tmp_dir}/K8S_CA_B64.txt" @@ -955,20 +1027,18 @@ clusters: server: ${server} tls-server-name: ${LAB_HOST_IP} contexts: - - name: gitea-postdeploy-validator@${CLUSTER_NAME} + - name: ${POSTDEPLOY_SERVICE_ACCOUNT}@${CLUSTER_NAME} context: cluster: ${CLUSTER_NAME} - namespace: ecommerce - user: gitea-postdeploy-validator -current-context: gitea-postdeploy-validator@${CLUSTER_NAME} + namespace: ${POSTDEPLOY_NAMESPACE} + user: ${POSTDEPLOY_SERVICE_ACCOUNT} +current-context: ${POSTDEPLOY_SERVICE_ACCOUNT}@${CLUSTER_NAME} users: - - name: gitea-postdeploy-validator + - name: ${POSTDEPLOY_SERVICE_ACCOUNT} user: token: ${token} 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" | tr -d '\n' \ > "${tmp_dir}/KUBE_CONFIG_DATA.txt" @@ -988,19 +1058,85 @@ EOF "${secret_dir}/${output_file}" done - # Los kubeconfig históricos también contienen credenciales. Ajustamos - # únicamente propietario y permisos; no asumimos ningún grupo llamado devops. - for output_file in kubeconfig-gitea.yaml lab-cluster.kubeconfig; do - if [[ -f "${secret_dir}/${output_file}" ]]; then - sudo_cmd chown "$owner" "${secret_dir}/${output_file}" - sudo_cmd chmod 0600 "${secret_dir}/${output_file}" - fi - done + sudo_cmd install \ + -m 0600 \ + -o "$owner" \ + "$kubeconfig_file" \ + "${secret_dir}/postdeploy-validator.kubeconfig" rm -rf "$tmp_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 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 require_commands @@ -2906,7 +3056,7 @@ main() { 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