228 lines
11 KiB
Markdown
228 lines
11 KiB
Markdown
# Hacer `entrypoint.sh` agnóstico de UID/GID (soporte `HOST_UID`/`HOST_GID`)
|
|
|
|
## Fix post-implementación: resync espurio con GID 0
|
|
|
|
**Síntoma observado:** al correr el contenedor aparece, repetido muchas veces,
|
|
`groupmod: GID '0' already exists`, además de la advertencia normal de
|
|
`adduser` sobre que el home dir ya existe.
|
|
|
|
**Causa raíz:** `entrypoint.sh` se re-ejecuta más de una vez en el ciclo de
|
|
arranque (el propio script hace `exec sudo -u $HOME_USER bash -c "...
|
|
/usr/bin/entrypoint.sh"` para re-lanzarse como el usuario destino, y además el
|
|
contenedor puede reiniciar el entrypoint). En cada ejecución se vuelve a
|
|
calcular `DETECTED_UID`/`DETECTED_GID` con `stat` sobre `/home/${HOME_USER}`.
|
|
En este entorno (bind mount vía WSL2/Docker Desktop), el `stat` del volumen
|
|
reporta GID `0` (root) — es un artefacto de metadata del mount, no el GID real
|
|
del usuario del host. Como la rama de "usuario ya existe" recalcula
|
|
`TARGET_GID` de la misma manera que la de creación, en cada re-arranque
|
|
posterior a la creación intenta `groupmod -g 0 aleleba`, lo cual falla siempre
|
|
porque el GID 0 ya pertenece a `root`.
|
|
|
|
**Fix acordado con el usuario:** para un usuario que **ya existe**, solo se
|
|
resincroniza UID/GID si `HOST_UID`/`HOST_GID` vienen seteados **explícitamente**
|
|
por variable de entorno. La auto-detección vía `stat` del volumen montado se
|
|
usa **únicamente** en la rama de creación de un usuario nuevo (para heredar la
|
|
propiedad de un home pre-existente), no en cada reinicio de un usuario que ya
|
|
existe. Además, se descarta cualquier UID/GID detectado que sea `0` (root)
|
|
como no confiable, cayendo al default `1000` en ese caso, tanto en creación
|
|
como al calcular el target explícito.
|
|
|
|
Esto preserva el caso de uso principal (`HOST_UID=502 HOST_GID=502` fija el
|
|
usuario a 502:502 tanto en la creación como en reinicios posteriores, sin
|
|
error) y elimina el crash cuando no se pasa override y el mount reporta
|
|
metadata espuria.
|
|
|
|
### Cambios concretos sobre el bloque ya implementado
|
|
|
|
1. Guardar contra UID/GID `0` al calcular `DETECTED_UID`/`DETECTED_GID` (cerca
|
|
de la línea 10-13 actual de `entrypoint.sh`):
|
|
```bash
|
|
DETECTED_UID=$(stat -c '%u' "/home/${HOME_USER}" 2>/dev/null || echo 1000)
|
|
DETECTED_GID=$(stat -c '%g' "/home/${HOME_USER}" 2>/dev/null || echo 1000)
|
|
[ "$DETECTED_UID" = "0" ] && DETECTED_UID=1000
|
|
[ "$DETECTED_GID" = "0" ] && DETECTED_GID=1000
|
|
TARGET_UID="${HOST_UID:-$DETECTED_UID}"
|
|
TARGET_GID="${HOST_GID:-$DETECTED_GID}"
|
|
```
|
|
(Si `HOST_UID`/`HOST_GID` se pasan explícitos, siguen ganando siempre,
|
|
incluso si son `0` — esa sería una elección deliberada del operador, no un
|
|
artefacto de mount.)
|
|
|
|
2. En la rama `else` (usuario ya existe) del bloque en `entrypoint.sh` (~línea
|
|
88 actual), solo intentar el resync si hay override explícito:
|
|
```bash
|
|
else
|
|
# Usuario ya existe (imagen/volumen reusado) — resincronizar UID/GID
|
|
# SOLO si el operador lo pidió explícitamente vía HOST_UID/HOST_GID.
|
|
# No usar la detección automática del mount acá: en reinicios repetidos
|
|
# es una señal poco confiable (ver metadata GID 0 de bind mounts en
|
|
# WSL2/Docker Desktop) y causaba un groupmod fallido en cada arranque.
|
|
if [[ -n "${HOST_UID-}" || -n "${HOST_GID-}" ]]; then
|
|
CURRENT_UID=$(id -u "${HOME_USER}")
|
|
CURRENT_GID=$(id -g "${HOME_USER}")
|
|
CURRENT_GROUP=$(id -gn "${HOME_USER}")
|
|
|
|
if [ "$CURRENT_UID" != "$TARGET_UID" ] || [ "$CURRENT_GID" != "$TARGET_GID" ]; then
|
|
if [ "$CURRENT_GID" != "$TARGET_GID" ]; then
|
|
sudo groupmod -g "${TARGET_GID}" "${CURRENT_GROUP}"
|
|
fi
|
|
if [ "$CURRENT_UID" != "$TARGET_UID" ]; then
|
|
sudo usermod -u "${TARGET_UID}" "${HOME_USER}"
|
|
fi
|
|
sudo find / -xdev \( -user "${CURRENT_UID}" -o -group "${CURRENT_GID}" \) \
|
|
-exec chown -h "${TARGET_UID}:${TARGET_GID}" {} + 2>/dev/null || true
|
|
fi
|
|
fi
|
|
fi
|
|
```
|
|
|
|
3. Actualizar la sección de `readme.md` sobre `HOST_UID`/`HOST_GID` para
|
|
aclarar que, para un usuario ya existente (contenedor reiniciado/reusado),
|
|
el resync solo ocurre si se pasan `HOST_UID`/`HOST_GID` explícitos — la
|
|
auto-detección desde el volumen montado solo aplica la primera vez que se
|
|
crea el usuario.
|
|
|
|
### Verificación del fix
|
|
1. `bash -n entrypoint.sh`.
|
|
2. Reiniciar el contenedor sin `HOST_UID`/`HOST_GID` sobre un volumen ya
|
|
existente (el mismo que dio el error) → no debe aparecer más
|
|
`groupmod: GID '0' already exists`, ni ningún intento de resync.
|
|
3. Reiniciar el contenedor con `-e HOST_UID=502 -e HOST_GID=502` sobre un
|
|
usuario ya creado con otro UID/GID → debe correr `usermod`/`groupmod` una
|
|
vez y quedar en 502:502, sin error.
|
|
4. Repetir el arranque una tercera vez con el mismo `HOST_UID=502
|
|
HOST_GID=502` → no debe intentar `usermod`/`groupmod` de nuevo (ya
|
|
coincide), y no debe haber errores.
|
|
|
|
## Contexto
|
|
|
|
Hoy `entrypoint.sh` crea el usuario `HOME_USER` (default `vscode`) siempre con
|
|
`--uid 1000` hardcodeado (línea 70). Esto rompe cuando el `/home/${HOME_USER}`
|
|
se monta desde un volumen del host cuyo dueño real tiene otro UID/GID (por
|
|
ejemplo un usuario Linux del host distinto de 1000), generando problemas de
|
|
permisos entre el contenedor y los archivos montados.
|
|
|
|
El objetivo es que el UID/GID del usuario dentro del contenedor:
|
|
1. Se pueda fijar explícitamente vía `HOST_UID` / `HOST_GID`.
|
|
2. Si no se fijan, se detecte automáticamente a partir del dueño real de
|
|
`/home/${HOME_USER}` (cuando ya existe por venir de un volumen montado).
|
|
3. Si tampoco se puede detectar, caiga al comportamiento actual (UID/GID 1000),
|
|
preservando compatibilidad con quien no use esta variable.
|
|
4. Si el contenedor se reinicia/reusa y el usuario del sistema ya existía con
|
|
otro UID/GID, se resincronice (`usermod -u`, `groupmod -g`) y se re-asignen
|
|
los archivos que quedaron con el UID/GID viejo, para no dejar huérfanos.
|
|
|
|
## Cambio 1 — detección de UID/GID (nueva, antes de tocar `/home`)
|
|
|
|
Insertar justo después de resolver `HOME_USER` (después de la línea 6 actual,
|
|
antes de que cualquier otra parte del script toque `/home/${HOME_USER}`).
|
|
|
|
Es importante hacerlo **ahí y no más abajo**: el loop de `USER_ENV_` (líneas
|
|
~48-56 actuales) puede crear `/home/${HOME_USER}` con dueño `root:root` *antes*
|
|
de llegar al bloque de creación de usuario, si hay variables `USER_ENV_*`
|
|
seteadas y el directorio no viene de un bind mount. Si la detección corriera
|
|
después de ese loop, leería `root:root` en vez de caer al default 1000.
|
|
|
|
```bash
|
|
DETECTED_UID=$(stat -c '%u' "/home/${HOME_USER}" 2>/dev/null || echo 1000)
|
|
DETECTED_GID=$(stat -c '%g' "/home/${HOME_USER}" 2>/dev/null || echo 1000)
|
|
TARGET_UID="${HOST_UID:-$DETECTED_UID}"
|
|
TARGET_GID="${HOST_GID:-$DETECTED_GID}"
|
|
```
|
|
|
|
Compatible con `set -eu`: `${VAR:-default}` no dispara `set -u`, y el `|| echo
|
|
1000` garantiza que la sustitución de `stat` nunca falle bajo `set -e`.
|
|
|
|
## Cambio 2 — reemplazar el bloque de creación de usuario (líneas 68-80 actuales)
|
|
|
|
```bash
|
|
USER="$HOME_USER"
|
|
if ! id -u "$HOME_USER" > /dev/null 2>&1; then
|
|
sudo groupadd -g "${TARGET_GID}" "${HOME_USER}" 2>/dev/null || true
|
|
sudo adduser --disabled-password --gecos "" --uid "${TARGET_UID}" --gid "${TARGET_GID}" "${HOME_USER}"
|
|
sudo echo "$HOME_USER ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers.d/nopasswd > /dev/null
|
|
|
|
# Creating .vscode folder if it doesn't exist
|
|
if [ ! -d "/home/${HOME_USER}/.vscode" ]; then
|
|
sudo mkdir -p "/home/${HOME_USER}/.vscode"
|
|
fi
|
|
|
|
# Changing the property of the directory /home/${HOME_USER}/.vscode
|
|
sudo chown -R "${HOME_USER}" "/home/${HOME_USER}/.vscode"
|
|
else
|
|
# Usuario ya existe (imagen/volumen reusado) — resincronizar UID/GID si cambiaron
|
|
CURRENT_UID=$(id -u "${HOME_USER}")
|
|
CURRENT_GID=$(id -g "${HOME_USER}")
|
|
CURRENT_GROUP=$(id -gn "${HOME_USER}")
|
|
|
|
if [ "$CURRENT_UID" != "$TARGET_UID" ] || [ "$CURRENT_GID" != "$TARGET_GID" ]; then
|
|
if [ "$CURRENT_GID" != "$TARGET_GID" ]; then
|
|
sudo groupmod -g "${TARGET_GID}" "${CURRENT_GROUP}"
|
|
fi
|
|
if [ "$CURRENT_UID" != "$TARGET_UID" ]; then
|
|
sudo usermod -u "${TARGET_UID}" "${HOME_USER}"
|
|
fi
|
|
sudo find / -xdev \( -user "${CURRENT_UID}" -o -group "${CURRENT_GID}" \) \
|
|
-exec chown -h "${TARGET_UID}:${TARGET_GID}" {} + 2>/dev/null || true
|
|
fi
|
|
fi
|
|
```
|
|
|
|
Notas de diseño:
|
|
- `groupadd -g "${TARGET_GID}" "${HOME_USER}"` corre siempre (con `|| true`)
|
|
antes del `adduser`, porque `adduser --gid <numérico>` requiere que ya
|
|
exista un grupo con ese GID.
|
|
- A diferencia del borrador original (que solo comparaba UID), también se
|
|
compara GID y se resincroniza con `groupmod` si cambió — si no, un volumen
|
|
cuyo GID cambió quedaría con archivos "group-orphan".
|
|
- `id -gn` resuelve el nombre real del grupo primario actual (por si no
|
|
coincide con `${HOME_USER}`), en vez de asumirlo.
|
|
- El resync de archivos hace un solo `find` con `-user X -o -group Y` y
|
|
`chown -h uid:gid`, cubriendo tanto el caso de UID como de GID en un solo
|
|
pase.
|
|
- Se mantiene sin cambios el `sudo find "/home/${HOME_USER}" -xdev -exec chown
|
|
"${HOME_USER}" {} +` que ya existe más abajo (línea ~86, en la rama donde el
|
|
script ya corre como el usuario objetivo) — no es redundante, es una red de
|
|
seguridad adicional sobre `/home`, y sigue siendo válida tanto si el usuario
|
|
se creó de cero como si se resincronizó.
|
|
|
|
### Riesgos conocidos (documentar, no bloquean el cambio)
|
|
- Si `TARGET_UID`/`TARGET_GID` ya están tomados por otra cuenta/grupo del
|
|
sistema, `usermod -u` / `groupmod -g` fallarán (comportamiento actual de
|
|
esas herramientas; no se agrega manejo especial).
|
|
- El `find / -xdev` de resincronización recorre todo el filesystem del
|
|
contenedor (limitado por `-xdev` a no cruzar de montaje) — tiene un costo de
|
|
arranque en imágenes con muchos archivos, pero es el mismo patrón que ya usa
|
|
el script en la línea 86.
|
|
|
|
## Cambio 3 — actualizar `readme.md` para documentar las nuevas env vars
|
|
|
|
- En la sección "Environment Variables" (líneas 13-18), agregar `HOST_UID` y
|
|
`HOST_GID` junto a `HOME_USER`/`VSCODE_TUNNEL_NAME`, explicando que son
|
|
opcionales y que si no se setean se autodetectan desde el volumen montado o
|
|
caen a 1000.
|
|
- En la sección "Using this image as a base image in a Dockerfile" (líneas
|
|
148-172), agregar una nota breve indicando que el UID `1000` de ese ejemplo
|
|
es solo ilustrativo y que en runtime `entrypoint.sh` puede resincronizarlo
|
|
vía `HOST_UID`/`HOST_GID` si se pasan al contenedor final.
|
|
|
|
No se toca el `Dockerfile` (el `fixuid` hardcodeado a `vscode` en la línea 22
|
|
es código muerto hoy — no se invoca en `entrypoint.sh` — y está fuera del
|
|
alcance de este cambio).
|
|
|
|
## Verificación
|
|
|
|
1. `bash -n entrypoint.sh` para chequeo de sintaxis.
|
|
2. Levantar el contenedor sin `HOST_UID`/`HOST_GID` y sin volumen montado en
|
|
`/home/vscode` → debe crear el usuario con UID/GID 1000 (comportamiento
|
|
actual preservado).
|
|
3. Levantar el contenedor con `-e HOST_UID=1500 -e HOST_GID=1500` → el usuario
|
|
creado debe tener ese UID/GID (`id vscode` dentro del contenedor).
|
|
4. Montar un volumen en `/home/vscode` cuyo contenido pertenezca a un UID/GID
|
|
distinto de 1000 (sin pasar `HOST_UID`/`HOST_GID`) → el usuario creado debe
|
|
tomar ese UID/GID detectado (`stat -c '%u:%g' /home/vscode`).
|
|
5. Simular "imagen reusada": crear el usuario una vez, luego reiniciar el
|
|
contenedor con un `HOST_UID`/`HOST_GID` distinto → verificar que
|
|
`usermod`/`groupmod` corrieron y que archivos de prueba en `/home/vscode`
|
|
quedaron con el nuevo dueño (`ls -n /home/vscode`).
|