arnaucube
arnaucube/configsPublic Self-hosted repository
configs/bin/box
bin/boxRaw
  1#!/usr/bin/env bash
  2#
  3# Launch Incus containers to work in there.
  4#
  5# Steps to do before first run on a new machine:
  6#
  7# sudo incus admin init
  8#
  9# then download the ubuntu image:
 10# sudo incus launch images:ubuntu/24.04 test
 11#
 12# - create a managed network bridge:
 13# incus network create incusbr0 ipv4.address=auto ipv4.nat=true ipv6.address=none
 14# incus profile device set default eth0 network incusbr0
 15#
 16#
 17# - if the container does not have internet access, at the host do:
 18#  sudo iptables -I DOCKER-USER 1 \
 19#   -i incusbr0 \
 20#   -s 10.159.38.0/24 \
 21#   -j ACCEPT
 22#
 23#  sudo iptables -I DOCKER-USER 2 \
 24#   -o incusbr0 \
 25#   -d 10.159.38.0/24 \
 26#   -m conntrack --ctstate RELATED,ESTABLISHED \
 27#   -j ACCEPT
 28
 29set -e
 30
 31if [ -z "${1:-}" ]; then
 32    echo "Error: no container name provided."
 33    echo "Usage: box <container_name> <profile_name(optional)>"
 34    exit 1
 35fi
 36
 37# Choose which container manager to use:
 38MNGR="sudo incus"
 39
 40# dev user with same permission levels as host user
 41DEV_USER="dev"
 42HOST_USER="${SUDO_USER:-$USER}"
 43HOST_UID=$(id -u "$HOST_USER")
 44HOST_GID=$(id -g "$HOST_USER")
 45HOST_HOME=$(getent passwd "$HOST_USER" | cut -d: -f6)
 46
 47CONTAINER_NAME="$1"
 48HOST_SHARE_DIR="$HOST_HOME/container_data/$CONTAINER_NAME"
 49CONTAINER_SHARE_DIR="/mnt/shared"
 50IMAGE="images:ubuntu/24.04"
 51
 52PROFILE_NAME=${2:-"default"} # if $2 is empty, use "default"
 53PROFILES_FILE="$HOST_HOME/bin/box_profiles.sh"
 54
 55# EXPOSE_PORTS="7000 7001 7002 7003 7004 7005 7006 7007 7008 7009 7010"
 56EXPOSE_PORTS=""
 57
 58
 59if [ -z "$CONTAINER_NAME" ]; then
 60    echo "error: no container name provided."
 61    echo "Usage: ./box.sh <container_name> [profile_name]"
 62    exit 1
 63fi
 64
 65if [ ! -f "$PROFILES_FILE" ]; then
 66    echo "error: $PROFILES_FILE not found."
 67    exit 1
 68fi
 69
 70source "$PROFILES_FILE"
 71
 72if ! declare -f "profile_$PROFILE_NAME" > /dev/null; then
 73    echo "error: profile '$PROFILE_NAME' not found in $PROFILES_FILE."
 74    echo "Available profiles are:"
 75    grep -oP '(?<=profile_).*(?=\(\))' "$PROFILES_FILE"
 76    exit 1
 77fi
 78
 79if $MNGR info "$CONTAINER_NAME" > /dev/null 2>&1; then
 80    echo "error: container '$CONTAINER_NAME' already exists."
 81    exit 1
 82fi
 83
 84echo "profile selected: [$PROFILE_NAME]"
 85
 86mkdir -p "$HOST_SHARE_DIR"
 87
 88echo "launch Incus container: $CONTAINER_NAME..."
 89# security.nesting=true allows Docker to run inside this container
 90$MNGR launch "$IMAGE" "$CONTAINER_NAME" -c security.nesting=true
 91
 92echo "wait for container"
 93for _ in $(seq 1 30); do
 94    if $MNGR exec "$CONTAINER_NAME" -- true > /dev/null 2>&1; then
 95        break
 96    fi
 97    sleep 1
 98done
 99
100if ! $MNGR exec "$CONTAINER_NAME" -- true > /dev/null 2>&1; then
101    echo "error: container did not become ready."
102    $MNGR delete -f "$CONTAINER_NAME"
103    exit 1
104fi
105
106echo "configure non-root development user"
107$MNGR exec "$CONTAINER_NAME" \
108    --env DEV_USER="$DEV_USER" \
109    --env HOST_UID="$HOST_UID" \
110    --env HOST_GID="$HOST_GID" \
111    -- bash -s <<'IN_CONTAINER'
112
113set -e
114
115# Find whether the desired UID already exists.
116EXISTING_USER=$(getent passwd "$HOST_UID" | cut -d: -f1 || true)
117
118if [ -n "$EXISTING_USER" ] && [ "$EXISTING_USER" != "$DEV_USER" ]; then
119    echo "renaming existing UID $HOST_UID user $EXISTING_USER -> $DEV_USER"
120
121    usermod -l "$DEV_USER" "$EXISTING_USER"
122
123elif [ -z "$EXISTING_USER" ]; then
124    echo "creating user $DEV_USER with UID $HOST_UID"
125
126    useradd \
127        -m \
128        -u "$HOST_UID" \
129        -s /bin/bash \
130        "$DEV_USER"
131fi
132
133# Move the user home to /home/dev.
134OLD_HOME=$(getent passwd "$DEV_USER" | cut -d: -f6)
135NEW_HOME="/home/$DEV_USER"
136
137if [ "$OLD_HOME" != "$NEW_HOME" ]; then
138    if [ -e "$NEW_HOME" ]; then
139        # In case the target directory already exists, copy the old home
140        # contents and point the user to the new location.
141        if [ -d "$OLD_HOME" ]; then
142            cp -a "$OLD_HOME"/. "$NEW_HOME"/
143        fi
144
145        usermod -d "$NEW_HOME" "$DEV_USER"
146    else
147        usermod -d "$NEW_HOME" -m "$DEV_USER"
148    fi
149fi
150
151usermod -s /bin/bash "$DEV_USER"
152
153# Match host primary GID as well.
154GROUP_NAME=$(getent group "$HOST_GID" | cut -d: -f1 || true)
155
156if [ -z "$GROUP_NAME" ]; then
157    groupadd -g "$HOST_GID" hostusers
158    GROUP_NAME=hostusers
159fi
160
161usermod -g "$GROUP_NAME" "$DEV_USER"
162
163# Make sure home belongs to dev.
164mkdir -p "/home/$DEV_USER"
165chown -R "$HOST_UID:$HOST_GID" "/home/$DEV_USER"
166chmod 755 "/home/$DEV_USER"
167
168echo "configured user:"
169id "$DEV_USER"
170
171IN_CONTAINER
172
173echo "configure shared directory (between container & host)"
174$MNGR config device add "$CONTAINER_NAME" project_storage disk \
175    source="$HOST_SHARE_DIR" \
176    path="$CONTAINER_SHARE_DIR"
177
178# shift=true maps the shared directory permissions between host and container
179$MNGR config device set "$CONTAINER_NAME" project_storage shift=true
180
181echo "configure port forwarding"
182for PORT in $EXPOSE_PORTS; do
183    $MNGR config device add "$CONTAINER_NAME" "proxy${PORT}" proxy \
184        listen="tcp:0.0.0.0:${PORT}" \
185        connect="tcp:127.0.0.1:${PORT}"
186done
187
188echo "wait for network"
189NETWORK_READY=false
190for _ in $(seq 1 30); do
191    if $MNGR exec "$CONTAINER_NAME" -- \
192        sh -c 'ip -4 route show default | grep -q "^default "'; then
193        NETWORK_READY=true
194        break
195    fi
196
197    sleep 1
198done
199if [ "$NETWORK_READY" != "true" ]; then
200    echo "error: container did not get a default IPv4 route."
201    echo "network state:"
202    $MNGR exec "$CONTAINER_NAME" -- ip addr || true
203    $MNGR exec "$CONTAINER_NAME" -- ip route || true
204    exit 1
205fi
206
207echo "check internet"
208INTERNET_READY=false
209for _ in $(seq 1 10); do
210    if $MNGR exec "$CONTAINER_NAME" -- ping -c 1 -W 2 8.8.8.8 > /dev/null 2>&1; then
211        INTERNET_READY=true
212        break
213    fi
214
215    sleep 1
216done
217if [ "$INTERNET_READY" != "true" ]; then
218    echo "error: container has a network route but no internet access."
219    echo "network state:"
220    $MNGR exec "$CONTAINER_NAME" -- ip addr || true
221    $MNGR exec "$CONTAINER_NAME" -- ip route || true
222    exit 1
223fi
224
225echo "apt update & apt install"
226{
227    # Load the profile functions inside the container.
228    cat "$PROFILES_FILE"
229
230    cat <<'IN_CONTAINER'
231
232set -e
233
234export DEBIAN_FRONTEND=noninteractive
235export NEEDRESTART_MODE=a # 'a' for automatic restart
236
237apt-get update
238apt-get install -y sudo
239apt-get upgrade -y
240
241# give user 'dev' sudo access
242usermod -aG sudo "$DEV_USER"
243
244printf '%s\n' "$DEV_USER ALL=(ALL) NOPASSWD:ALL" \
245    > "/etc/sudoers.d/$DEV_USER"
246
247chmod 0440 "/etc/sudoers.d/$DEV_USER"
248
249if declare -f "profile_$PROFILE_NAME" > /dev/null; then
250    "profile_$PROFILE_NAME"
251else
252    echo "error: profile function $PROFILE_NAME not found."
253    exit 1
254fi
255
256# profile_docker might have created the docker group
257if getent group docker > /dev/null; then
258    usermod -aG docker "$DEV_USER"
259fi
260
261IN_CONTAINER
262
263} | $MNGR exec "$CONTAINER_NAME" \
264    --env DEV_USER="$DEV_USER" \
265    --env PROFILE_NAME="$PROFILE_NAME" \
266    -- bash -s
267
268echo "configure development environment as $DEV_USER"
269$MNGR exec "$CONTAINER_NAME" \
270    -- su -l "$DEV_USER" -c 'bash -s' <<'IN_DEV'
271
272set -e
273
274cd "$HOME"
275
276if [ ! -d "$HOME/configs/.git" ]; then
277    git clone https://github.com/arnaucube/configs.git "$HOME/configs"
278fi
279
280cd "$HOME/configs"
281
282# yes | bash install-new-server.sh
283
284IN_DEV
285
286echo "configure npm global packages as $DEV_USER"
287$MNGR exec "$CONTAINER_NAME" \
288    -- su -l "$DEV_USER" -c 'bash -s' <<'IN_DEV'
289
290set -e
291
292mkdir -p "$HOME/.local"
293
294# install global npm packages under the dev home instead of /usr/lib
295npm config set prefix "$HOME/.local"
296
297PATH_LINE='export PATH="$HOME/.local/bin:$PATH"'
298
299if ! grep -qxF "$PATH_LINE" "$HOME/.bashrc" 2>/dev/null; then
300    echo "$PATH_LINE" >> "$HOME/.bashrc"
301fi
302
303export PATH="$HOME/.local/bin:$PATH"
304
305IN_DEV
306
307echo "configure shared directory permissions"
308
309# Make existing files in the shared directory belong to the host/dev user.
310sudo chown -R "$HOST_UID:$HOST_GID" "$HOST_SHARE_DIR"
311
312# directories: rwxr-xr-x
313find "$HOST_SHARE_DIR" \
314    -type d \
315    -exec chmod 755 {} +
316
317# normal files: rw-r--r--
318find "$HOST_SHARE_DIR" \
319    -type f \
320    ! -perm /111 \
321    -exec chmod 644 {} +
322
323# preserve executable files as executable
324find "$HOST_SHARE_DIR" \
325    -type f \
326    -perm /111 \
327    -exec chmod 755 {} +
328
329echo "configure Incus shell to use non-root user"
330
331# This alias is global for the current Incus client.
332# Containers entered with `incus shell` are expected to have the dev user.
333$MNGR alias remove shell > /dev/null 2>&1 || true
334$MNGR alias add shell "exec @ARGS@ -- su -l $DEV_USER"
335
336echo "check configuration"
337$MNGR exec "$CONTAINER_NAME" \
338    -- su -l "$DEV_USER" -c '
339        echo "user: $(whoami)"
340        echo "home: $HOME"
341        echo "uid/gid: $(id)"
342        echo "working directory: $(pwd)"
343    '
344
345echo "check shared directory permissions"
346$MNGR exec "$CONTAINER_NAME" \
347    -- su -l "$DEV_USER" -c '
348        touch /mnt/shared/.box-permission-test
349        rm /mnt/shared/.box-permission-test
350    '
351
352echo "setup complete"
353echo "----------------------------------------------------"
354echo "Shared Directory (Host): $HOST_SHARE_DIR"
355echo "Shared Directory (Container): $CONTAINER_SHARE_DIR"
356echo "----------------------------------------------------"
357echo "To enter the container, run: $MNGR shell $CONTAINER_NAME"
358echo "To enter as root, run: $MNGR exec $CONTAINER_NAME -- bash"
359echo "To stop the container, run: $MNGR stop $CONTAINER_NAME"
360echo "To remove the container, run: $MNGR delete $CONTAINER_NAME"