Ejecución local de Docker y propagación de señal CTRL-C

Ejecución local de Docker y propagación de señal CTRL-C

Tengo un sistema de envío de clústeres basado en Docker y estoy intentando que también admita la ejecución local. Cuando se ejecuta localmente, el comando que inicia el trabajo es básicamente

docker run /results/src/launcher/local.sh

Para la ejecución del clúster, se ejecuta otro script. La dificultad a la que me enfrento es cómo ejecutar el código como usuario local y al mismo tiempo admitir CTRL-C correctamente. Dado que Docker Run inicia el punto de entrada como uid 0, necesito ejecutar el punto de entrada del usuario con su -c. Básicamente, el script necesita ejecutar dos cosas:

  1. Un script previo a la ejecución (llamado root)
  2. Un programa Python (llamado usuario llamante)

El meollo del guión es actualmente el siguiente:

# Run prerun script
$PRERUN &
PRERUN_PID=$!
wait $PRERUN_PID
PRERUN_FINISHED=true
status=$?

if [ "$status" -eq "0" ]; then
    echo "Prerun finished successfully."
else
    echo "Prerun failed with code: $status"
    exit $status
fi

# Run main program dropping root privileges.
su -c '/opt/conda/bin/python /results/src/launcher/entrypoint.py \
      > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)' \
      $USER &
PYTHON_PID=$!
wait $PYTHON_PID
PYTHON_FINISHED=true
status=$?

if [ "$status" -eq "0" ]; then
    echo "Entrypoint finished successfully."
else
    echo "Entrypoint failed with code: $status"
    exit $status 
fi

La propagación de la señal se maneja en el mismo script mediante:

_int() {
    echo "Caught SIGINT signal!"
    if [ "$PRERUN_PID" -ne "0" ] && [ "$PRERUN_FINISHED" = "false" ]; then
        echo "Sending SIGINT to prerun script!"
        kill -INT $PRERUN_PID
        PRERUN_PID=0
    fi
    if [ "$PYTHON_PID" -ne "0" ] && [ "$PYTHON_FINISHED" = "false" ]; then
        echo "Sending SIGINT to Python entrypoint!"
        kill -INT $PYTHON_PID
        PYTHON_PID=0
    fi
}

PRERUN_PID=0
PYTHON_PID=0
PRERUN_FINISHED=false
PYTHON_FINISHED=false
trap _int SIGINT

Tengo un controlador de señales /results/src/launcher/entrypoint.py, que es el código ejecutado por su -c. Sin embargo, nunca parece obtener el SIGINT. Supongo que el problema está en el su -c. Como era de esperar, PYTHON_PIDen el script bash no se asigna el PID del intérprete de Python, sino del suprograma. Si hago un os.system("ps xa")en mi punto de entrada de Python, veo lo siguiente:

  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:00 /bin/bash /results/src/launcher/local.sh user 1000 1000 /results/src/example/compile.sh
   61 ?        S      0:00 su -c /opt/conda/bin/python /results/src/launcher/entrypoint.py \       > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2) user
   62 ?        Ss     0:00 bash -c /opt/conda/bin/python /results/src/launcher/entrypoint.py \       > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)
   66 ?        S      0:01 /opt/conda/bin/python /results/src/launcher/entrypoint.py
   67 ?        S      0:00 bash -c /opt/conda/bin/python /results/src/launcher/entrypoint.py \       > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)
   68 ?        S      0:00 bash -c /opt/conda/bin/python /results/src/launcher/entrypoint.py \       > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)
   69 ?        S      0:00 tee -a /results/stdout.txt
   70 ?        S      0:00 tee -a /results/stderr.txt
   82 ?        R      0:00 /opt/conda/bin/python /results/src/launcher/entrypoint.py
   83 ?        S      0:00 /bin/dash -c ps xa
   84 ?        R      0:00 ps xa

PYTHON_PIDse le asigna el PID 61. Sin embargo, me gustaría poder apagar correctamente el intérprete de Python, por lo que debería poder captar alguna señal allí. ¿Alguien sabe cómo reenviar un SIGINT al intérprete de Python en una situación como esta? ¿Habría una manera más inteligente de hacer lo que estoy tratando de lograr? Tengo control total sobre el código que crea el docker runcomando cuando el código está programado para ejecución local.

Respuesta1

Están sucediendo algunas cosas aquí. Primero, está ejecutando un script de shell como pid 1 dentro del contenedor. Ese proceso en varios escenarios es lo que ve el cont+c, o docker stopel envío de la señal, y depende de bash atraparlo y manejarlo. De forma predeterminada, cuando se ejecuta como pid 1, bash ignorará la señal (creo que maneja el modo de usuario único en un servidor Linux). Necesitarías capturar y manejar explícitamente esa señal con algo como:

trap 'pkill -P $$; exit 1;' TERM INT

en la parte superior del guión. Eso detectaría SIGTERM y SIGINT (generados por cont+c), eliminaría los procesos secundarios y saldría inmediatamente.

A continuación, está el sucomando, que a su vez bifurca un proceso que puede interrumpir el manejo de la señal. Prefiero gosuque ejecute un ejecutivo en lugar de una llamada al sistema fork, eliminándose de la lista de procesos. Puede instalar gosucon lo siguiente en un Dockerfile:

ARG GOSU_VER=1.10
ARG GOSU_ARCH=amd64
RUN curl -sSL "https://github.com/tianon/gosu/releases/download/${GOSU_VER}/gosu-${GOSU_ARCH}" >/usr/bin/gosu \
 && chmod 755 /usr/bin/gosu \
 && gosu nobody true

Por último, hay mucha lógica en el punto de entrada para bifurcarse y luego esperar a que finalice un proceso en segundo plano. Esto podría simplificarse ejecutando los procesos en primer plano. El último comando que ejecute se puede iniciar con un execpara evitar dejar el shell ejecutándose. Puede detectar errores con set -eo expandirlo para mostrar la depuración de qué comandos se ejecutan con una -xbandera. El resultado final se parece a:

#!/bin/bash

set -ex

# in case a signal is received during PRERUN
trap 'exit 1;' TERM INT

# Run prerun script
$PRERUN

# Run main program dropping root privileges.
exec gosu "$USER" /opt/conda/bin/python /results/src/launcher/entrypoint.py \
      > >(tee -a /results/stdout.txt) 2> >(tee -a /results/stderr.txt >&2)

Si puede deshacerse de los /resultsregistros, debería poder cambiar desde /bin/bashla /bin/shparte superior del script y simplemente confiar en docker logsver los resultados del contenedor.

información relacionada