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