Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/_common/README.md
Original file line number Diff line number Diff line change
@@ -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")
```
87 changes: 87 additions & 0 deletions src/_common/common-setup.sh
Original file line number Diff line number Diff line change
@@ -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}"
}
20 changes: 5 additions & 15 deletions src/anaconda/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 12 additions & 19 deletions src/common-utils/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
20 changes: 5 additions & 15 deletions src/conda/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 5 additions & 15 deletions src/desktop-lite/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
20 changes: 5 additions & 15 deletions src/docker-in-docker/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
20 changes: 5 additions & 15 deletions src/docker-outside-of-docker/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading