diff --git a/src/_common/README.md b/src/_common/README.md new file mode 100644 index 000000000..64ea15c66 --- /dev/null +++ b/src/_common/README.md @@ -0,0 +1,122 @@ +# Common Helper Scripts + +This directory contains common helper scripts that can be shared across multiple features to avoid code duplication. + +## common-setup.sh + +A helper script that provides common setup functions used across multiple features. + +### Functions + +#### `determine_user_from_input` + +Determines the appropriate non-root user based on the input username. + +**Usage:** +```bash +# Source the helper script +source "${SCRIPT_DIR}/../_common/common-setup.sh" + +# Determine the user +USERNAME=$(determine_user_from_input "${USERNAME}" "root") +``` + +**Parameters:** +- `$1` (required): Input username from feature configuration (e.g., "automatic", "auto", "none", or a specific username) +- `$2` (optional): Fallback username when no user is found in automatic mode (defaults to "root") + +**Behavior:** +- **"auto" or "automatic"**: + - First checks if `_REMOTE_USER` environment variable is set and is not "root" + - If `_REMOTE_USER` is root or not set, searches for an existing user from the priority list: + 1. `devcontainer` + 2. `vscode` + 3. `node` + 4. `codespace` + 5. User with UID 1000 (from `/etc/passwd`) + - If no user is found, returns the fallback user (default: "root") + +- **"none"**: Always returns "root" + +- **Specific username**: + - Validates the user exists using `id -u` + - If the user exists, returns that username + - If the user doesn't exist, returns "root" + +**Examples:** + +```bash +# Basic usage with default fallback (root) +USERNAME=$(determine_user_from_input "automatic") + +# With custom fallback +USERNAME=$(determine_user_from_input "automatic" "vscode") + +# Explicit user +USERNAME=$(determine_user_from_input "myuser") + +# None (always returns root) +USERNAME=$(determine_user_from_input "none") +``` + +**Return Value:** +Prints the resolved username to stdout, which can be captured using command substitution. + +## Testing + +Tests for the helper scripts are located in `/test/_common/`. Run the tests with: + +```bash +bash test/_common/test-common-setup.sh +``` + +## Edge Cases + +The helper handles several edge cases: + +1. **Missing awk**: Some systems (like Mariner) don't have awk by default. Features should install it before sourcing the helper if needed. + +2. **UID 1000 lookup**: The user with UID 1000 is included in the search as it's commonly the first non-system user created. + +3. **_REMOTE_USER behavior**: When `_REMOTE_USER` is set to a non-root user, it takes priority over all other detection methods in automatic mode. + +4. **Empty user list entries**: The helper safely handles empty entries in the user detection loop. + +## Migration Guide + +To migrate an existing feature to use the common helper: + +### Before: +```bash +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi +``` + +### After: +```bash +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + +# Determine the appropriate non-root user +USERNAME=$(determine_user_from_input "${USERNAME}" "root") +``` + +**Note:** For features like `common-utils` that create users and need a different fallback, use: +```bash +USERNAME=$(determine_user_from_input "${USERNAME}" "vscode") +``` diff --git a/src/_common/common-setup.sh b/src/_common/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/_common/common-setup.sh @@ -0,0 +1,87 @@ +#!/bin/bash +#------------------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://github.com/devcontainers/features/blob/main/LICENSE for license information. +#------------------------------------------------------------------------------------------------------------------------- +# +# Helper script for common feature setup tasks, including user selection logic. +# Maintainer: The Dev Container spec maintainers + +# Determine the appropriate non-root user +# Usage: determine_user_from_input USERNAME [FALLBACK_USER] +# +# This function resolves the USERNAME variable based on the input value: +# - If USERNAME is "auto" or "automatic", it will detect an existing non-root user +# - If USERNAME is "none" or doesn't exist, it will fall back to root +# - Otherwise, it validates the specified USERNAME exists +# +# Arguments: +# USERNAME - The username input (typically from feature configuration) +# FALLBACK_USER - Optional fallback user when no user is found in automatic mode (defaults to "root") +# +# Returns: +# The resolved username is printed to stdout +# +# Examples: +# USERNAME=$(determine_user_from_input "automatic") +# USERNAME=$(determine_user_from_input "vscode") +# USERNAME=$(determine_user_from_input "auto" "vscode") +# +determine_user_from_input() { + local input_username="${1:-automatic}" + local fallback_user="${2:-root}" + local resolved_username="" + + if [ "${input_username}" = "auto" ] || [ "${input_username}" = "automatic" ]; then + # Automatic mode: try to detect an existing non-root user + + # First, check if _REMOTE_USER is set and is not root + if [ -n "${_REMOTE_USER:-}" ] && [ "${_REMOTE_USER}" != "root" ]; then + # Verify the user exists before using it + if id -u "${_REMOTE_USER}" > /dev/null 2>&1; then + resolved_username="${_REMOTE_USER}" + else + # _REMOTE_USER doesn't exist, fall through to normal detection + resolved_username="" + fi + fi + + # If we didn't resolve via _REMOTE_USER, try to find a non-root user + if [ -z "${resolved_username}" ]; then + # Try to find a non-root user from a list of common usernames + # The list includes: devcontainer, vscode, node, codespace, and the user with UID 1000 + local possible_users=("devcontainer" "vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '')") + + for current_user in "${possible_users[@]}"; do + # Skip empty entries + if [ -z "${current_user}" ]; then + continue + fi + + # Check if user exists + if id -u "${current_user}" > /dev/null 2>&1; then + resolved_username="${current_user}" + break + fi + done + + # If no user found, use the fallback + if [ -z "${resolved_username}" ]; then + resolved_username="${fallback_user}" + fi + fi + elif [ "${input_username}" = "none" ]; then + # Explicit "none" means use root + resolved_username="root" + else + # Specific username provided - validate it exists + if id -u "${input_username}" > /dev/null 2>&1; then + resolved_username="${input_username}" + else + # User doesn't exist, fall back to root + resolved_username="root" + fi + fi + + echo "${resolved_username}" +} diff --git a/src/anaconda/install.sh b/src/anaconda/install.sh index 6f57a3144..01b7b15ef 100755 --- a/src/anaconda/install.sh +++ b/src/anaconda/install.sh @@ -81,22 +81,12 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u "${CURRENT_USER}" > /dev/null 2>&1; then - USERNAME="${CURRENT_USER}" - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") architecture="$(uname -m)" # Normalize arm64 to aarch64 for consistency diff --git a/src/common-utils/main.sh b/src/common-utils/main.sh index b0fd2f3b0..eaa4cb512 100644 --- a/src/common-utils/main.sh +++ b/src/common-utils/main.sh @@ -399,27 +399,20 @@ case "${ADJUSTED_ID}" in ;; esac -# If in automatic mode, determine if a user already exists, if not use vscode -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - if [ "${_REMOTE_USER}" != "root" ]; then - USERNAME="${_REMOTE_USER}" - else - USERNAME="" - POSSIBLE_USERS=("devcontainer" "vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=vscode - fi - fi -elif [ "${USERNAME}" = "none" ]; then - USERNAME=root +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + +# Handle the special "none" case for common-utils before user determination +# The "none" case sets USER_UID and USER_GID to 0 +ORIGINAL_USERNAME="${USERNAME}" +if [ "${ORIGINAL_USERNAME}" = "none" ]; then + USERNAME="root" USER_UID=0 USER_GID=0 +else + # If in automatic mode, determine if a user already exists, if not use vscode (which will be created) + USERNAME=$(determine_user_from_input "${USERNAME}" "vscode") fi # Create or update a non-root user to match UID/GID. group_name="${USERNAME}" diff --git a/src/conda/install.sh b/src/conda/install.sh index 43ab82f54..20e4feeac 100644 --- a/src/conda/install.sh +++ b/src/conda/install.sh @@ -27,22 +27,12 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u "${CURRENT_USER}" > /dev/null 2>&1; then - USERNAME="${CURRENT_USER}" - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") architecture="$(uname -m)" if [ "${architecture}" != "x86_64" ]; then diff --git a/src/desktop-lite/install.sh b/src/desktop-lite/install.sh index 4575cc4f9..3f366b354 100755 --- a/src/desktop-lite/install.sh +++ b/src/desktop-lite/install.sh @@ -71,22 +71,12 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") # Add default Fluxbox config files if none are already present fluxbox_apps="$(cat \ << 'EOF' diff --git a/src/docker-in-docker/install.sh b/src/docker-in-docker/install.sh index 3f30158e5..db365d394 100755 --- a/src/docker-in-docker/install.sh +++ b/src/docker-in-docker/install.sh @@ -44,22 +44,12 @@ fi # See: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/shared/utils.sh ################### +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") # Package manager update function pkg_mgr_update() { diff --git a/src/docker-outside-of-docker/install.sh b/src/docker-outside-of-docker/install.sh index 74fd63530..034dde234 100755 --- a/src/docker-outside-of-docker/install.sh +++ b/src/docker-outside-of-docker/install.sh @@ -38,22 +38,12 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") apt_get_update() { diff --git a/src/go/install.sh b/src/go/install.sh index 85fea5dc4..f4628b37e 100755 --- a/src/go/install.sh +++ b/src/go/install.sh @@ -174,22 +174,12 @@ if ! type awk >/dev/null 2>&1; then check_packages awk fi +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") export DEBIAN_FRONTEND=noninteractive diff --git a/src/hugo/install.sh b/src/hugo/install.sh index b268e384b..bbfd2bb13 100755 --- a/src/hugo/install.sh +++ b/src/hugo/install.sh @@ -29,22 +29,12 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") architecture="$(uname -m)" if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then diff --git a/src/java/install.sh b/src/java/install.sh index 62fd39462..f1d13d201 100644 --- a/src/java/install.sh +++ b/src/java/install.sh @@ -154,22 +154,12 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") updaterc() { local _bashrc diff --git a/src/java/wrapper.sh b/src/java/wrapper.sh index bea343f96..202a1856d 100644 --- a/src/java/wrapper.sh +++ b/src/java/wrapper.sh @@ -23,19 +23,6 @@ if [ "${is_jdk_8}" = "true" ]; then ./install.sh "${ADDITIONAL_JAVA_VERSION}" "${SDKMAN_DIR}" "${USERNAME}" "${UPDATE_RC}" jdk_11_folder="$(ls --format=single-column ${SDKMAN_DIR}/candidates/java | grep -oE -m 1 '11\..+')" ln -s "${SDKMAN_DIR}/candidates/java/${jdk_11_folder}" /extension-java-home - - # Determine the appropriate non-root user - username="" - possible_users=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for current_user in "${POSSIBLE_USERS[@]}"; do - if id -u ${current_user} > /dev/null 2>&1; then - username=${current_user} - break - fi - done - if [ "${username}" = "" ]; then - username=root - fi else ln -s ${SDKMAN_DIR}/candidates/java/current /extension-java-home fi diff --git a/src/kubectl-helm-minikube/install.sh b/src/kubectl-helm-minikube/install.sh index f0cf1c946..cbafb18e9 100755 --- a/src/kubectl-helm-minikube/install.sh +++ b/src/kubectl-helm-minikube/install.sh @@ -28,22 +28,12 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") USERHOME="/home/$USERNAME" if [ "$USERNAME" = "root" ]; then diff --git a/src/node/install.sh b/src/node/install.sh index 71d91ffe2..e241d47d7 100755 --- a/src/node/install.sh +++ b/src/node/install.sh @@ -247,22 +247,12 @@ if ! type awk >/dev/null 2>&1; then check_packages awk fi +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") # Ensure apt is in non-interactive to avoid prompts export DEBIAN_FRONTEND=noninteractive diff --git a/src/oryx/install.sh b/src/oryx/install.sh index cf67db6b1..b43580b23 100755 --- a/src/oryx/install.sh +++ b/src/oryx/install.sh @@ -25,22 +25,12 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") function updaterc() { if [ "${UPDATE_RC}" = "true" ]; then diff --git a/src/php/install.sh b/src/php/install.sh index 357395e88..4eea3a4ed 100755 --- a/src/php/install.sh +++ b/src/php/install.sh @@ -36,24 +36,12 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh -# If in automatic mode, determine if a user already exists, if not use vscode -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ]; then - USERNAME=root - USER_UID=0 - USER_GID=0 -fi +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + +# If in automatic mode, determine if a user already exists, if not use root +USERNAME=$(determine_user_from_input "${USERNAME}" "root") architecture="$(uname -m)" if [ "${architecture}" != "amd64" ] && [ "${architecture}" != "x86_64" ] && [ "${architecture}" != "arm64" ] && [ "${architecture}" != "aarch64" ]; then diff --git a/src/python/install.sh b/src/python/install.sh index be895927d..86345bd34 100755 --- a/src/python/install.sh +++ b/src/python/install.sh @@ -835,22 +835,12 @@ if ! type awk >/dev/null 2>&1; then check_packages awk fi +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") # Ensure apt is in non-interactive to avoid prompts export DEBIAN_FRONTEND=noninteractive diff --git a/src/ruby/install.sh b/src/ruby/install.sh index 39cb5be03..87a6d7384 100755 --- a/src/ruby/install.sh +++ b/src/ruby/install.sh @@ -39,22 +39,12 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") updaterc() { if [ "${UPDATE_RC}" = "true" ]; then diff --git a/src/rust/install.sh b/src/rust/install.sh index 99a7ba8f5..54bcd1748 100755 --- a/src/rust/install.sh +++ b/src/rust/install.sh @@ -101,22 +101,12 @@ rm -f /etc/profile.d/00-restore-env.sh echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh chmod +x /etc/profile.d/00-restore-env.sh +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u "${CURRENT_USER}" > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u "${USERNAME}" > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") # Figure out correct version of a three part version number is not passed find_version_from_git_tags() { diff --git a/src/sshd/install.sh b/src/sshd/install.sh index 9b9ddedf2..281d095b4 100755 --- a/src/sshd/install.sh +++ b/src/sshd/install.sh @@ -25,22 +25,12 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +# Source common helper functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../_common/common-setup.sh" + # Determine the appropriate non-root user -if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then - USERNAME="" - POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") - for CURRENT_USER in "${POSSIBLE_USERS[@]}"; do - if id -u ${CURRENT_USER} > /dev/null 2>&1; then - USERNAME=${CURRENT_USER} - break - fi - done - if [ "${USERNAME}" = "" ]; then - USERNAME=root - fi -elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then - USERNAME=root -fi +USERNAME=$(determine_user_from_input "${USERNAME}" "root") apt_get_update() { diff --git a/test/_common/test-common-setup.sh b/test/_common/test-common-setup.sh new file mode 100755 index 000000000..149571e50 --- /dev/null +++ b/test/_common/test-common-setup.sh @@ -0,0 +1,221 @@ +#!/bin/bash +#------------------------------------------------------------------------------------------------------------------------- +# Tests for common-setup.sh helper functions +# These tests validate the determine_user_from_input function +#------------------------------------------------------------------------------------------------------------------------- + +set -e + +# Source the helper script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../../src/_common/common-setup.sh" + +# Test counters +PASSED=0 +FAILED=0 +TOTAL=0 + +# Helper function to run a test +run_test() { + local test_name="$1" + local expected="$2" + local actual="$3" + + TOTAL=$((TOTAL + 1)) + + if [ "${expected}" = "${actual}" ]; then + echo "✓ PASS: ${test_name}" + PASSED=$((PASSED + 1)) + else + echo "✗ FAIL: ${test_name}" + echo " Expected: '${expected}'" + echo " Actual: '${actual}'" + FAILED=$((FAILED + 1)) + fi +} + +# Test 1: Automatic mode finds existing user or fallback +test_automatic_no_users() { + local result=$(determine_user_from_input "automatic") + # Should find either a known user or fallback to root + # On this system, there may be a UID 1000 user (like packer) + local uid_1000_user=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '') + local expected="${uid_1000_user:-root}" + run_test "Automatic mode with no matching common users finds UID 1000 or root" "${expected}" "${result}" +} + +# Test 2: Automatic mode with fallback user +test_automatic_with_fallback() { + local result=$(determine_user_from_input "automatic" "vscode") + # Should find a user or use the fallback - check if UID 1000 exists + local uid_1000_user=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '') + local expected="${uid_1000_user:-vscode}" + run_test "Automatic mode with custom fallback finds UID 1000 or uses fallback" "${expected}" "${result}" +} + +# Test 3: Explicit "none" should return root +test_none_returns_root() { + local result=$(determine_user_from_input "none") + run_test "Explicit 'none' returns root" "root" "${result}" +} + +# Test 4: Explicit "none" ignores fallback +test_none_ignores_fallback() { + local result=$(determine_user_from_input "none" "vscode") + run_test "Explicit 'none' ignores fallback" "root" "${result}" +} + +# Test 5: Existing user (root) should return root +test_existing_user_root() { + local result=$(determine_user_from_input "root") + run_test "Existing user 'root' returns root" "root" "${result}" +} + +# Test 6: Non-existing user should return root +test_nonexisting_user() { + local result=$(determine_user_from_input "nonexistentuser12345") + run_test "Non-existing user returns root" "root" "${result}" +} + +# Test 7: Auto mode (synonym for automatic) +test_auto_synonym() { + local result=$(determine_user_from_input "auto" "customfallback") + # Should behave same as automatic - find UID 1000 or use fallback + local uid_1000_user=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '') + local expected="${uid_1000_user:-customfallback}" + run_test "Auto mode with fallback finds UID 1000 or uses fallback" "${expected}" "${result}" +} + +# Test 8: _REMOTE_USER environment variable (when set and not root) +test_remote_user_set() { + # Test with an existing user (root is always available) + # We'll use a user that exists on the system + local existing_user=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo 'root') + + export _REMOTE_USER="${existing_user}" + local result=$(determine_user_from_input "automatic") + unset _REMOTE_USER + + run_test "_REMOTE_USER set to non-root user" "${existing_user}" "${result}" +} + +# Test 9: _REMOTE_USER set to root should use fallback logic +test_remote_user_root() { + export _REMOTE_USER="root" + local result=$(determine_user_from_input "automatic" "mydefault") + unset _REMOTE_USER + + # Should use fallback logic - find UID 1000 or use fallback + local uid_1000_user=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '') + local expected="${uid_1000_user:-mydefault}" + run_test "_REMOTE_USER set to root uses fallback logic" "${expected}" "${result}" +} + +# Test 10: Finding vscode user if it exists +test_find_vscode_user() { + # Check if vscode user exists and no higher priority users exist + if id -u vscode > /dev/null 2>&1 && \ + ! id -u devcontainer > /dev/null 2>&1; then + # Unset _REMOTE_USER to ensure it doesn't interfere + unset _REMOTE_USER + local result=$(determine_user_from_input "automatic") + # Should find vscode (it's second in priority after devcontainer) + run_test "Finds vscode user in automatic mode" "vscode" "${result}" + else + # Skip this test if vscode user doesn't exist or higher priority user exists + run_test "Finds vscode user in automatic mode (SKIPPED - conditions not met)" "SKIP" "SKIP" + fi +} + +# Test 11: Finding devcontainer user (highest priority) +test_find_devcontainer_user() { + # Check if devcontainer user exists + if id -u devcontainer > /dev/null 2>&1; then + # Unset _REMOTE_USER to ensure it doesn't interfere + unset _REMOTE_USER + local result=$(determine_user_from_input "automatic") + # Should find devcontainer (highest priority) + run_test "Finds devcontainer user (highest priority)" "devcontainer" "${result}" + else + # Skip this test if devcontainer user doesn't exist + run_test "Finds devcontainer user (highest priority) (SKIPPED - user doesn't exist)" "SKIP" "SKIP" + fi +} + +# Test 12: Finding user with UID 1000 +test_find_uid_1000() { + # Check if there's a user with UID 1000 + local uid_1000_user=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '') + + if [ -n "${uid_1000_user}" ] && \ + ! id -u devcontainer > /dev/null 2>&1 && \ + ! id -u vscode > /dev/null 2>&1 && \ + ! id -u node > /dev/null 2>&1 && \ + ! id -u codespace > /dev/null 2>&1; then + # Only test if UID 1000 exists and no higher priority users exist + local result=$(determine_user_from_input "automatic") + run_test "Finds user with UID 1000" "${uid_1000_user}" "${result}" + else + # Skip this test if conditions aren't met + run_test "Finds user with UID 1000 (SKIPPED - conditions not met)" "SKIP" "SKIP" + fi +} + +# Test 13: Empty input defaults to "automatic" +test_empty_input() { + local result=$(determine_user_from_input "" "mydefault") + # Should behave as automatic mode - find UID 1000 or use fallback + local uid_1000_user=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '') + local expected="${uid_1000_user:-mydefault}" + run_test "Empty input treated as automatic and finds UID 1000 or uses fallback" "${expected}" "${result}" +} + +# Test 14: _REMOTE_USER set to non-existent user should use fallback +test_remote_user_nonexistent() { + export _REMOTE_USER="nonexistentuser99999" + local result=$(determine_user_from_input "automatic" "mydefault") + unset _REMOTE_USER + + # Should fall through to normal detection - find UID 1000 or use fallback + local uid_1000_user=$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd 2>/dev/null || echo '') + local expected="${uid_1000_user:-mydefault}" + run_test "_REMOTE_USER set to non-existent user falls back to detection" "${expected}" "${result}" +} + +# Run all tests +echo "Running tests for common-setup.sh..." +echo "======================================" +echo "" + +test_automatic_no_users +test_automatic_with_fallback +test_none_returns_root +test_none_ignores_fallback +test_existing_user_root +test_nonexisting_user +test_auto_synonym +test_remote_user_set +test_remote_user_root +test_find_vscode_user +test_find_devcontainer_user +test_find_uid_1000 +test_empty_input +test_remote_user_nonexistent + +# Print summary +echo "" +echo "======================================" +echo "Test Summary:" +echo " Total: ${TOTAL}" +echo " Passed: ${PASSED}" +echo " Failed: ${FAILED}" +echo "======================================" + +# Exit with appropriate code +if [ ${FAILED} -eq 0 ]; then + echo "All tests passed! ✓" + exit 0 +else + echo "Some tests failed! ✗" + exit 1 +fi