#!/bin/bash # Script para redeployar stacks en Portainer, actualizando todas sus imágenes # Implementa filosofía GitOps para despliegue continuo # Soporta stacks basados en archivos y stacks conectados a repositorios Git # Configuración por defecto PRUNE_IMAGES=false # Función para mostrar ayuda function show_help { echo "Uso: $0 [opciones]" echo "" echo "Opciones:" echo " --url URL URL de Portainer (obligatorio)" echo " --username USUARIO Nombre de usuario de Portainer (obligatorio)" echo " --password PASSWORD Contraseña de Portainer (obligatorio)" echo " --environment ENTORNO Nombre del entorno en Portainer (obligatorio)" echo " --stack STACK Nombre del stack a actualizar (obligatorio)" echo " --prune Eliminar imágenes no utilizadas después del despliegue" echo " --help Mostrar esta ayuda" exit 1 } # Verificar que los comandos necesarios estén disponibles if ! command -v curl &> /dev/null; then echo "Error: curl no está instalado. Por favor, instálalo e inténtalo de nuevo." exit 1 fi if ! command -v jq &> /dev/null; then echo "Error: jq no está instalado. Por favor, instálalo e inténtalo de nuevo." exit 1 fi # Procesar argumentos while [[ $# -gt 0 ]]; do case $1 in --url) PORTAINER_URL="${2}" shift 2 ;; --username) PORTAINER_USERNAME="${2}" shift 2 ;; --password) PORTAINER_PASSWORD="${2}" shift 2 ;; --environment) ENVIRONMENT_NAME="${2}" shift 2 ;; --stack) STACK_NAME="${2}" shift 2 ;; --prune) PRUNE_IMAGES=true shift ;; --help) show_help ;; *) echo "Opción desconocida: $1" show_help ;; esac done # Verificar argumentos obligatorios if [ -z "$PORTAINER_URL" ] || [ -z "$PORTAINER_USERNAME" ] || [ -z "$PORTAINER_PASSWORD" ] || [ -z "$ENVIRONMENT_NAME" ] || [ -z "$STACK_NAME" ]; then echo "Error: Faltan argumentos obligatorios." show_help fi # Eliminar la barra final de la URL si existe PORTAINER_URL=${PORTAINER_URL%/} # Función para autenticar con Portainer function authenticate { echo "Autenticando con Portainer..." AUTH_RESPONSE=$(curl -s -X POST \ "$PORTAINER_URL/api/auth" \ -H "Content-Type: application/json" \ -d "{\"Username\":\"$PORTAINER_USERNAME\",\"Password\":\"$PORTAINER_PASSWORD\"}") if [ $? -ne 0 ]; then echo "Error al conectar con Portainer" exit 1 fi # Extraer token JWT JWT_TOKEN=$(echo $AUTH_RESPONSE | jq -r '.jwt') if [ "$JWT_TOKEN" == "null" ] || [ -z "$JWT_TOKEN" ]; then echo "Error de autenticación. Verifica tus credenciales." exit 1 fi echo "Autenticación exitosa" } # Función para obtener el ID del entorno function get_environment_id { echo "Buscando entorno '$ENVIRONMENT_NAME'..." ENVIRONMENTS_RESPONSE=$(curl -s -X GET \ "$PORTAINER_URL/api/endpoints" \ -H "Authorization: Bearer $JWT_TOKEN") ENVIRONMENT_ID=$(echo $ENVIRONMENTS_RESPONSE | jq -r ".[] | select(.Name == \"$ENVIRONMENT_NAME\" ) | .Id") if [ -z "$ENVIRONMENT_ID" ]; then echo "Error: Entorno '$ENVIRONMENT_NAME' no encontrado" exit 1 fi echo "Entorno '$ENVIRONMENT_NAME' encontrado con ID: $ENVIRONMENT_ID" } # Función para obtener stacks function get_stacks { echo "Obteniendo stacks del entorno..." STACKS_RESPONSE=$(curl -s -X GET \ "$PORTAINER_URL/api/stacks" \ -H "Authorization: Bearer $JWT_TOKEN") if [ $? -ne 0 ]; then echo "Error al obtener stacks" exit 1 fi # Filtrar stacks por ID de entorno ENVIRONMENT_STACKS=$(echo $STACKS_RESPONSE | jq -c "[.[] | select(.EndpointId == $ENVIRONMENT_ID)]") # Contar total de stacks STACK_COUNT=$(echo $ENVIRONMENT_STACKS | jq '. | length') echo "Encontrados $STACK_COUNT stacks en el entorno" } # Función para encontrar el stack por nombre function find_target_stack { echo "Buscando stack '$STACK_NAME'..." TARGET_STACK=$(echo $ENVIRONMENT_STACKS | jq -c "[.[] | select(.Name == \"$STACK_NAME\")][0]") if [ "$(echo $TARGET_STACK | jq -r '.')" == "null" ]; then echo "Error: Stack '$STACK_NAME' no encontrado en el entorno '$ENVIRONMENT_NAME'" exit 1 fi STACK_ID=$(echo $TARGET_STACK | jq -r '.Id') STACK_ENV=$(echo $TARGET_STACK | jq -r '.Env') STACK_ENDPOINT=$(echo $TARGET_STACK | jq -r '.EndpointId') STACK_TYPE=$(echo $TARGET_STACK | jq -r '.GitConfig | if . == null then "file" else "git" end') echo "Stack '$STACK_NAME' encontrado con ID: $STACK_ID (Tipo: $STACK_TYPE)" } # Función para actualizar un stack basado en archivo function update_file_stack { echo "Actualizando stack basado en archivo..." curl -s -X GET \ "$PORTAINER_URL/api/stacks/$STACK_ID/file" \ -H "Authorization: Bearer $JWT_TOKEN" > /tmp/stack_details.json # Extraer información relevante STACK_CONTENT=$(jq -r '.StackFileContent' /tmp/stack_details.json) # Escapar correctamente el contenido del archivo para JSON STACK_CONTENT_ESCAPED=$(echo "$STACK_CONTENT" | jq -Rs .) # Construir cuerpo de la solicitud para actualización UPDATE_BODY=$(cat << EOF { "stackFileContent": ${STACK_CONTENT_ESCAPED}, "env": $STACK_ENV, "prune": true, "pullImage": true } EOF ) # Actualizar stack echo "Redesplegando stack $STACK_NAME con pull de todas las imágenes..." UPDATE_RESPONSE=$(curl -s -X PUT \ "$PORTAINER_URL/api/stacks/$STACK_ID?endpointId=$STACK_ENDPOINT" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $JWT_TOKEN" \ -d "$UPDATE_BODY") if [ $? -ne 0 ] || [ "$(echo $UPDATE_RESPONSE | jq -r '.message // empty')" != "" ]; then echo "Error al actualizar stack: $(echo $UPDATE_RESPONSE | jq -r '.message // "Error desconocido"')" exit 1 fi echo "Stack actualizado correctamente" } # Función para actualizar un stack basado en Git function update_git_stack { echo "Actualizando stack conectado a Git..." # Obtener detalles del stack STACK_DETAILS=$(curl -s -X GET \ "$PORTAINER_URL/api/stacks/$STACK_ID" \ -H "Authorization: Bearer $JWT_TOKEN") # Extraer información Git GIT_CONFIG=$(echo $STACK_DETAILS | jq '.GitConfig') AUTO_UPDATE=$(echo $GIT_CONFIG | jq '.AutoUpdate') # Construir payload para actualización UPDATE_BODY=$(cat << EOF { "env": $(echo $STACK_DETAILS | jq '.Env'), "prune": true, "pullImage": true } EOF ) # Para stacks Git, usamos el endpoint específico para pull echo "Solicitando pull de repositorio Git y actualización de imágenes..." UPDATE_RESPONSE=$(curl -s -X POST \ "$PORTAINER_URL/api/stacks/$STACK_ID/git/redeploy?endpointId=$ENVIRONMENT_ID" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $JWT_TOKEN" \ -d "$UPDATE_BODY") if [ $? -ne 0 ] || [ "$(echo $UPDATE_RESPONSE | jq -r '.message // empty')" != "" ]; then echo "Error al actualizar stack Git: $(echo $UPDATE_RESPONSE | jq -r '.message // "Error desconocido"')" exit 1 fi echo "Stack Git actualizado correctamente" } # Función para limpiar imágenes no utilizadas function prune_images { if [ "$PRUNE_IMAGES" = true ]; then echo "Eliminando imágenes no utilizadas..." PRUNE_RESPONSE=$(curl -s -X POST \ "$PORTAINER_URL/api/endpoints/$ENVIRONMENT_ID/docker/images/prune?filters=%7B%22dangling%22%3A%7B%22true%22%3Atrue%7D%7D" \ -H "Authorization: Bearer $JWT_TOKEN") if [ $? -eq 0 ]; then SPACE_RECLAIMED=$(echo $PRUNE_RESPONSE | jq -r '.SpaceReclaimed') SPACE_MB=$(echo "scale=2; $SPACE_RECLAIMED / 1024 / 1024" | bc) echo "Limpieza completada. Espacio recuperado: $SPACE_MB MB" else echo "Error durante la limpieza de imágenes" fi fi } # Función para verificar el estado del stack después de la actualización function verify_stack_status { echo "Verificando estado del stack..." # Esperar un momento para que el stack se actualice sleep 5 # Obtener detalles actualizados del stack STACK_STATUS=$(curl -s -X GET \ "$PORTAINER_URL/api/stacks/$STACK_ID" \ -H "Authorization: Bearer $JWT_TOKEN") # Verificar el estado STACK_STATUS_VALUE=$(echo $STACK_STATUS | jq -r '.Status') if [ "$STACK_STATUS_VALUE" == "1" ]; then echo "Stack '$STACK_NAME' actualizado y en ejecución correctamente" else echo "Advertencia: El stack puede no estar completamente activo. Status: $STACK_STATUS_VALUE" echo "Verifica el estado en la interfaz de Portainer para más detalles" fi } # Ejecución principal authenticate get_environment_id get_stacks find_target_stack # Actualizar según el tipo de stack if [ "$STACK_TYPE" == "file" ]; then update_file_stack elif [ "$STACK_TYPE" == "git" ]; then update_git_stack fi # Acciones posteriores verify_stack_status prune_images echo "" echo "Proceso de actualización de stack completado" exit 0