diff --git a/SHARED_CODE.md b/SHARED_CODE.md new file mode 100644 index 000000000..7ec9479be --- /dev/null +++ b/SHARED_CODE.md @@ -0,0 +1,94 @@ +# Shared Code Maintenance + +This document explains how shared code is maintained across features in this repository. + +## Problem + +Multiple features need the same helper functions (e.g., user selection logic). The devcontainer specification currently packages each feature independently and doesn't support sharing code between features at runtime. + +## Solution + +We maintain a **single source of truth** with a **sync mechanism** to deploy to each feature: + +### Single Source +- **Location**: `scripts/lib/common-setup.sh` +- **Contains**: Shared helper functions (currently user selection logic) +- **Maintenance**: All updates happen here + +### Deployment +- **Mechanism**: `scripts/sync-common-setup.sh` +- **Target**: Copies to each feature's `_lib/` directory +- **Reason**: Devcontainer packaging requires files to be within each feature's directory + +## Workflow + +### Making Changes + +1. **Edit the source**: Modify `scripts/lib/common-setup.sh` +2. **Test**: Run `bash test/_lib/test-common-setup.sh` +3. **Sync**: Run `./scripts/sync-common-setup.sh` +4. **Commit**: Include both source and deployed copies + +```bash +# Edit the source +vim scripts/lib/common-setup.sh + +# Test +bash test/_lib/test-common-setup.sh + +# Deploy to all features +./scripts/sync-common-setup.sh + +# Commit everything +git add scripts/lib/common-setup.sh src/*/_lib/common-setup.sh +git commit -m "Update common-setup.sh helper function" +``` + +### Verification + +The sync script is idempotent - running it multiple times with the same source produces the same result. After syncing, you can verify: + +```bash +# Check that all copies are identical +for f in src/*/_lib/common-setup.sh; do + diff -q scripts/lib/common-setup.sh "$f" || echo "MISMATCH: $f" +done +``` + +## Why Not Use Shared Files? + +The devcontainer CLI packages each feature independently. When a feature is installed: + +1. Only files within the feature's directory are included in the package +2. Parent directories (`../common`) are not accessible +3. Hidden directories (`.common`) are excluded from packaging +4. Sibling feature directories are not accessible + +This is a design decision in the devcontainer specification to ensure features are portable and self-contained. + +## Future + +The devcontainer spec has a proposal for an `include` property in `devcontainer-feature.json` ([spec#129](https://github.com/devcontainers/spec/issues/129)) that would enable native code sharing. Once implemented, the sync mechanism can be removed in favor of declarative includes: + +```json +{ + "id": "my-feature", + "include": ["../../scripts/lib/common-setup.sh"] +} +``` + +## Current Implementation + +As of this PR: +- **Source**: `scripts/lib/common-setup.sh` (87 lines) +- **Deployed**: 17 features, each with `src/FEATURE/_lib/common-setup.sh` +- **Sync Script**: `scripts/sync-common-setup.sh` +- **Tests**: `test/_lib/test-common-setup.sh` (14 test cases) +- **Benefits**: Eliminated ~188 lines of inline duplicated logic from install scripts + +## References + +- [Devcontainer Spec Issue #129 - Share code between features](https://github.com/devcontainers/spec/issues/129) +- [Features Library Proposal](https://github.com/devcontainers/spec/blob/main/proposals/features-library.md) +- Test documentation: `test/_lib/README.md` +- Sync script documentation: `scripts/README.md` diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..4f295cb73 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,59 @@ +# Shared Feature Code + +This directory contains code that is shared across multiple features. + +## Structure + +``` +scripts/ +├── lib/ +│ └── common-setup.sh # Source of truth for user selection helper +└── sync-common-setup.sh # Script to deploy helper to all features +``` + +## Maintenance + +### The Source of Truth + +**`scripts/lib/common-setup.sh`** is the single source of truth for the user selection helper function. All modifications should be made to this file. + +### Deploying Changes + +Due to the devcontainer CLI's packaging behavior (each feature is packaged independently), the helper must be deployed to each feature's `_lib/` directory. We maintain this through a sync script: + +```bash +./scripts/sync-common-setup.sh +``` + +This copies `scripts/lib/common-setup.sh` to all features: +- `src/anaconda/_lib/common-setup.sh` +- `src/docker-in-docker/_lib/common-setup.sh` +- etc. + +### Workflow + +1. **Edit**: Make changes to `scripts/lib/common-setup.sh` +2. **Test**: Run `bash test/_lib/test-common-setup.sh` to verify +3. **Sync**: Run `./scripts/sync-common-setup.sh` to deploy to all features +4. **Commit**: Commit both the source and all copies together + +### Why Copies? + +The devcontainer CLI packages each feature independently: +- Parent directories are not included in the build context +- Hidden directories (`.common`) are not included +- Sibling directories are not accessible + +Therefore, each feature needs its own copy of the helper to ensure it's available at runtime during feature installation. + +## Testing + +Tests are located in `test/_lib/` and reference the anaconda feature's copy as the source: + +```bash +bash test/_lib/test-common-setup.sh +``` + +## Future + +This approach is a workaround for the current limitation. The devcontainer spec has a proposal for an `include` property in `devcontainer-feature.json` that would allow native code sharing (see [devcontainers/spec#129](https://github.com/devcontainers/spec/issues/129)). Once implemented, this sync mechanism can be removed. diff --git a/scripts/lib/common-setup.sh b/scripts/lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/scripts/lib/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/scripts/sync-common-setup.sh b/scripts/sync-common-setup.sh new file mode 100755 index 000000000..034005bee --- /dev/null +++ b/scripts/sync-common-setup.sh @@ -0,0 +1,75 @@ +#!/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. +#------------------------------------------------------------------------------------------------------------------------- +# +# Script to sync common-setup.sh from the source to all feature _lib directories +# This maintains a single source of truth while deploying to each feature for packaging +# +# Usage: ./scripts/sync-common-setup.sh +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +# The source of truth for common-setup.sh +SOURCE_FILE="${REPO_ROOT}/scripts/lib/common-setup.sh" + +# Features that use the common-setup helper +# Note: common-utils is excluded because it creates users (different semantics) +FEATURES=( + "anaconda" + "conda" + "desktop-lite" + "docker-in-docker" + "docker-outside-of-docker" + "go" + "hugo" + "java" + "kubectl-helm-minikube" + "node" + "oryx" + "php" + "python" + "ruby" + "rust" + "sshd" +) + +echo "Syncing common-setup.sh to all features..." +echo "Source: ${SOURCE_FILE}" +echo "" + +if [ ! -f "${SOURCE_FILE}" ]; then + echo "Error: Source file not found: ${SOURCE_FILE}" + exit 1 +fi + +UPDATED_COUNT=0 + +for feature in "${FEATURES[@]}"; do + TARGET_DIR="${REPO_ROOT}/src/${feature}/_lib" + TARGET_FILE="${TARGET_DIR}/common-setup.sh" + + # Create _lib directory if it doesn't exist + mkdir -p "${TARGET_DIR}" + + # Copy the file + cp "${SOURCE_FILE}" "${TARGET_FILE}" + + echo "✓ Synced to src/${feature}/_lib/common-setup.sh" + UPDATED_COUNT=$((UPDATED_COUNT + 1)) +done + +echo "" +echo "======================================" +echo "Sync complete!" +echo "Updated ${UPDATED_COUNT} features" +echo "======================================" +echo "" +echo "Note: After running this script, commit the changes:" +echo " git add src/*/lib/common-setup.sh" +echo " git commit -m 'Sync common-setup.sh to all features'" diff --git a/src/anaconda/_lib/common-setup.sh b/src/anaconda/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/anaconda/_lib/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/devcontainer-feature.json b/src/anaconda/devcontainer-feature.json index ac76a9b99..5b714c74c 100644 --- a/src/anaconda/devcontainer-feature.json +++ b/src/anaconda/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "anaconda", - "version": "1.1.0", + "version": "1.2.0", "name": "Anaconda", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/anaconda", "options": { diff --git a/src/anaconda/install.sh b/src/anaconda/install.sh index 6f57a3144..dd2e523be 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}/_lib/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/devcontainer-feature.json b/src/common-utils/devcontainer-feature.json index 14056e3a9..712f2b3ca 100644 --- a/src/common-utils/devcontainer-feature.json +++ b/src/common-utils/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "common-utils", - "version": "2.5.6", + "version": "2.6.0", "name": "Common Utilities", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/common-utils", "description": "Installs a set of common command line utilities, Oh My Zsh!, and sets up a non-root user.", diff --git a/src/common-utils/main.sh b/src/common-utils/main.sh index b0fd2f3b0..95316035f 100644 --- a/src/common-utils/main.sh +++ b/src/common-utils/main.sh @@ -399,11 +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 +# 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 +elif [ "${ORIGINAL_USERNAME}" = "auto" ] || [ "${ORIGINAL_USERNAME}" = "automatic" ]; then + # If in automatic mode, determine if a user already exists, if not use vscode (which will be created) + # common-utils has special handling because it CREATES users, not just uses existing ones if [ "${_REMOTE_USER}" != "root" ]; then USERNAME="${_REMOTE_USER}" else + # Try to find an existing user, or fall back to "vscode" which we'll create 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 @@ -416,10 +425,6 @@ if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then USERNAME=vscode fi fi -elif [ "${USERNAME}" = "none" ]; then - USERNAME=root - USER_UID=0 - USER_GID=0 fi # Create or update a non-root user to match UID/GID. group_name="${USERNAME}" diff --git a/src/conda/_lib/common-setup.sh b/src/conda/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/conda/_lib/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/conda/devcontainer-feature.json b/src/conda/devcontainer-feature.json index 163696a20..cd590f9b7 100644 --- a/src/conda/devcontainer-feature.json +++ b/src/conda/devcontainer-feature.json @@ -1,43 +1,43 @@ { - "id": "conda", - "version": "1.0.10", - "name": "Conda", - "description": "A cross-platform, language-agnostic binary package manager", - "documentationURL": "https://github.com/devcontainers/features/tree/main/src/conda", - "options": { - "version": { - "type": "string", - "proposals": [ - "latest", - "4.11.0", - "4.12.0" - ], - "default": "latest", - "description": "Select or enter a conda version." - }, - "addCondaForge": { - "type": "boolean", - "default": false, - "description": "Add conda-forge channel to the config?" - } + "id": "conda", + "version": "1.2.5", + "name": "Conda", + "description": "A cross-platform, language-agnostic binary package manager", + "documentationURL": "https://github.com/devcontainers/features/tree/main/src/conda", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "4.11.0", + "4.12.0" + ], + "default": "latest", + "description": "Select or enter a conda version." }, - "containerEnv": { - "CONDA_DIR": "/opt/conda", - "CONDA_SCRIPT":"/opt/conda/etc/profile.d/conda.sh", - "PATH": "/opt/conda/bin:${PATH}" - }, - "customizations": { - "vscode": { - "settings": { - "github.copilot.chat.codeGeneration.instructions": [ - { - "text": "This dev container includes the conda package manager pre-installed and available on the `PATH` for data science and Python development. Additional packages installed using Conda will be downloaded from Anaconda or another repository configured by the user. A user can install different versions of Python than the one in this dev container by running a command like: conda install python=3.7" - } - ] - } - } - }, - "installsAfter": [ - "ghcr.io/devcontainers/features/common-utils" - ] + "addCondaForge": { + "type": "boolean", + "default": false, + "description": "Add conda-forge channel to the config?" + } + }, + "containerEnv": { + "CONDA_DIR": "/opt/conda", + "CONDA_SCRIPT": "/opt/conda/etc/profile.d/conda.sh", + "PATH": "/opt/conda/bin:${PATH}" + }, + "customizations": { + "vscode": { + "settings": { + "github.copilot.chat.codeGeneration.instructions": [ + { + "text": "This dev container includes the conda package manager pre-installed and available on the `PATH` for data science and Python development. Additional packages installed using Conda will be downloaded from Anaconda or another repository configured by the user. A user can install different versions of Python than the one in this dev container by running a command like: conda install python=3.7" + } + ] + } + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] } diff --git a/src/conda/install.sh b/src/conda/install.sh index 43ab82f54..ef3b6010f 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}/_lib/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 @@ -83,20 +73,78 @@ if ! conda --version &> /dev/null ; then usermod -a -G conda "${USERNAME}" # Install dependencies - check_packages curl ca-certificates gnupg2 + check_packages curl ca-certificates echo "Installing Conda..." - curl -sS https://repo.anaconda.com/pkgs/misc/gpgkeys/anaconda.asc | gpg --dearmor > /usr/share/keyrings/conda-archive-keyring.gpg - echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/conda-archive-keyring.gpg] https://repo.anaconda.com/pkgs/misc/debrepo/conda stable main" > /etc/apt/sources.list.d/conda.list - apt-get update -y - - CONDA_PKG="conda=${VERSION}-0" + # Download .deb package directly from repository (bypassing SHA1 signature issue) + TEMP_DEB="$(mktemp -t conda_XXXXXX.deb)" + CONDA_REPO_BASE="https://repo.anaconda.com/pkgs/misc/debrepo/conda" + + # Determine package filename based on requested version + ARCH="$(dpkg --print-architecture 2>/dev/null || echo "amd64")" + PACKAGES_URL="https://repo.anaconda.com/pkgs/misc/debrepo/conda/dists/stable/main/binary-${ARCH}/Packages" + if [ "${VERSION}" = "latest" ]; then - CONDA_PKG="conda" + # For latest, we need to query the repository to find the current version + echo "Fetching package list to determine latest version..." + CONDA_PKG_INFO=$(curl -fsSL "${PACKAGES_URL}" | grep -A 30 "^Package: conda$" | head -n 31) + CONDA_VERSION=$(echo "${CONDA_PKG_INFO}" | grep "^Version:" | head -n 1 | awk '{print $2}') + CONDA_FILENAME=$(echo "${CONDA_PKG_INFO}" | grep "^Filename:" | head -n 1 | awk '{print $2}') + + if [ -z "${CONDA_VERSION}" ] || [ -z "${CONDA_FILENAME}" ]; then + echo "ERROR: Could not determine latest conda version or filename from ${PACKAGES_URL}" + echo "This may indicate an unsupported architecture or repository unavailability." + rm -f "${TEMP_DEB}" + exit 1 + fi + + CONDA_PKG_NAME="${CONDA_FILENAME}" + else + # For specific versions, query the Packages file to find the exact filename + echo "Fetching package list to find version ${VERSION}..." + # Search for version pattern - user may specify 4.12.0 but package has 4.12.0-0 + CONDA_PKG_INFO=$(curl -fsSL "${PACKAGES_URL}" | grep -A 30 "^Package: conda$" | grep -B 5 -A 25 "^Version: ${VERSION}") + CONDA_FILENAME=$(echo "${CONDA_PKG_INFO}" | grep "^Filename:" | head -n 1 | awk '{print $2}') + + if [ -z "${CONDA_FILENAME}" ]; then + echo "ERROR: Could not find conda version ${VERSION} in ${PACKAGES_URL}" + echo "Please verify the version specified is valid." + rm -f "${TEMP_DEB}" + exit 1 + fi + + CONDA_PKG_NAME="${CONDA_FILENAME}" fi - - check_packages $CONDA_PKG + + # Download the .deb package + CONDA_DEB_URL="${CONDA_REPO_BASE}/${CONDA_PKG_NAME}" + echo "Downloading conda package from ${CONDA_DEB_URL}..." + + if ! curl -fsSL "${CONDA_DEB_URL}" -o "${TEMP_DEB}"; then + echo "ERROR: Failed to download conda .deb package from ${CONDA_DEB_URL}" + echo "Please verify the version specified is valid." + rm -f "${TEMP_DEB}" + exit 1 + fi + + # Verify the package was downloaded successfully + if [ ! -f "${TEMP_DEB}" ] || [ ! -s "${TEMP_DEB}" ]; then + echo "ERROR: Conda .deb package file is missing or empty" + rm -f "${TEMP_DEB}" + exit 1 + fi + + # Install the package using apt (which handles dependencies automatically) + echo "Installing conda package..." + if ! apt-get install -y "${TEMP_DEB}"; then + echo "ERROR: Failed to install conda package" + rm -f "${TEMP_DEB}" + exit 1 + fi + + # Clean up downloaded package + rm -f "${TEMP_DEB}" CONDA_SCRIPT="/opt/conda/etc/profile.d/conda.sh" . $CONDA_SCRIPT diff --git a/src/desktop-lite/_lib/common-setup.sh b/src/desktop-lite/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/desktop-lite/_lib/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/desktop-lite/devcontainer-feature.json b/src/desktop-lite/devcontainer-feature.json index ae10c9977..aa2a6c68f 100644 --- a/src/desktop-lite/devcontainer-feature.json +++ b/src/desktop-lite/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "desktop-lite", - "version": "1.2.8", + "version": "1.3.0", "name": "Light-weight Desktop", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/desktop-lite", "description": "Adds a lightweight Fluxbox based desktop to the container that can be accessed using a VNC viewer or the web. GUI-based commands executed from the built-in VS code terminal will open on the desktop automatically.", diff --git a/src/desktop-lite/install.sh b/src/desktop-lite/install.sh index 4575cc4f9..47ae44857 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}/_lib/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/_lib/common-setup.sh b/src/docker-in-docker/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/docker-in-docker/_lib/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/docker-in-docker/devcontainer-feature.json b/src/docker-in-docker/devcontainer-feature.json index 56520a200..48d807552 100644 --- a/src/docker-in-docker/devcontainer-feature.json +++ b/src/docker-in-docker/devcontainer-feature.json @@ -1,94 +1,94 @@ { - "id": "docker-in-docker", - "version": "2.14.0", - "name": "Docker (Docker-in-Docker)", - "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-in-docker", - "description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.", - "options": { - "version": { - "type": "string", - "proposals": [ - "latest", - "none", - "20.10" - ], - "default": "latest", - "description": "Select or enter a Docker/Moby Engine version. (Availability can vary by OS version.)" - }, - "moby": { - "type": "boolean", - "default": true, - "description": "Install OSS Moby build instead of Docker CE" - }, - "mobyBuildxVersion": { - "type": "string", - "default": "latest", - "description": "Install a specific version of moby-buildx when using Moby" - }, - "dockerDashComposeVersion": { - "type": "string", - "enum": [ - "none", - "v1", - "v2" - ], - "default": "v2", - "description": "Default version of Docker Compose (v1, v2 or none)" - }, - "azureDnsAutoDetection": { - "type": "boolean", - "default": true, - "description": "Allow automatically setting the dockerd DNS server when the installation script detects it is running in Azure" - }, - "dockerDefaultAddressPool": { - "type": "string", - "default": "", - "proposals": [], - "description": "Define default address pools for Docker networks. e.g. base=192.168.0.0/16,size=24" - }, - "installDockerBuildx": { - "type": "boolean", - "default": true, - "description": "Install Docker Buildx" - }, - "installDockerComposeSwitch": { - "type": "boolean", - "default": false, - "description": "Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter." - }, - "disableIp6tables": { - "type": "boolean", - "default": false, - "description": "Disable ip6tables (this option is only applicable for Docker versions 27 and greater)" - } + "id": "docker-in-docker", + "version": "2.16.0", + "name": "Docker (Docker-in-Docker)", + "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-in-docker", + "description": "Create child containers *inside* a container, independent from the host's docker instance. Installs Docker extension in the container along with needed CLIs.", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "none", + "20.10" + ], + "default": "latest", + "description": "Select or enter a Docker/Moby Engine version. (Availability can vary by OS version.)" }, - "entrypoint": "/usr/local/share/docker-init.sh", - "privileged": true, - "containerEnv": { - "DOCKER_BUILDKIT": "1" + "moby": { + "type": "boolean", + "default": true, + "description": "Install OSS Moby build instead of Docker CE" }, - "customizations": { - "vscode": { - "extensions": [ - "ms-azuretools.vscode-containers" - ], - "settings": { - "github.copilot.chat.codeGeneration.instructions": [ - { - "text": "This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container." - } - ] - } - } + "mobyBuildxVersion": { + "type": "string", + "default": "latest", + "description": "Install a specific version of moby-buildx when using Moby" }, - "mounts": [ - { - "source": "dind-var-lib-docker-${devcontainerId}", - "target": "/var/lib/docker", - "type": "volume" - } - ], - "installsAfter": [ - "ghcr.io/devcontainers/features/common-utils" - ] + "dockerDashComposeVersion": { + "type": "string", + "enum": [ + "none", + "v1", + "v2" + ], + "default": "v2", + "description": "Default version of Docker Compose (v1, v2 or none)" + }, + "azureDnsAutoDetection": { + "type": "boolean", + "default": true, + "description": "Allow automatically setting the dockerd DNS server when the installation script detects it is running in Azure" + }, + "dockerDefaultAddressPool": { + "type": "string", + "default": "", + "proposals": [], + "description": "Define default address pools for Docker networks. e.g. base=192.168.0.0/16,size=24" + }, + "installDockerBuildx": { + "type": "boolean", + "default": true, + "description": "Install Docker Buildx" + }, + "installDockerComposeSwitch": { + "type": "boolean", + "default": false, + "description": "Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter." + }, + "disableIp6tables": { + "type": "boolean", + "default": false, + "description": "Disable ip6tables (this option is only applicable for Docker versions 27 and greater)" + } + }, + "entrypoint": "/usr/local/share/docker-init.sh", + "privileged": true, + "containerEnv": { + "DOCKER_BUILDKIT": "1" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.vscode-containers" + ], + "settings": { + "github.copilot.chat.codeGeneration.instructions": [ + { + "text": "This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using a dedicated Docker daemon running inside the dev container." + } + ] + } + } + }, + "mounts": [ + { + "source": "dind-var-lib-docker-${devcontainerId}", + "target": "/var/lib/docker", + "type": "volume" + } + ], + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] } diff --git a/src/docker-in-docker/install.sh b/src/docker-in-docker/install.sh index 3f30158e5..de6aa872d 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}/_lib/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() { @@ -303,6 +293,13 @@ if ! command -v git >/dev/null 2>&1; then check_packages git fi +# Update CA certificates to ensure HTTPS connections work properly +# This is especially important for Ubuntu 24.04 (Noble) and Debian Trixie +# Only run for Debian-based systems (RHEL uses update-ca-trust instead) +if [ "${ADJUSTED_ID}" = "debian" ] && command -v update-ca-certificates > /dev/null 2>&1; then + update-ca-certificates +fi + # Swap to legacy iptables for compatibility (Debian only) if [ "${ADJUSTED_ID}" = "debian" ] && type iptables-legacy > /dev/null 2>&1; then update-alternatives --set iptables /usr/sbin/iptables-legacy diff --git a/src/docker-outside-of-docker/_lib/common-setup.sh b/src/docker-outside-of-docker/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/docker-outside-of-docker/_lib/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/docker-outside-of-docker/devcontainer-feature.json b/src/docker-outside-of-docker/devcontainer-feature.json index 7314fa83d..d0039a843 100644 --- a/src/docker-outside-of-docker/devcontainer-feature.json +++ b/src/docker-outside-of-docker/devcontainer-feature.json @@ -1,80 +1,80 @@ { - "id": "docker-outside-of-docker", - "version": "1.6.5", - "name": "Docker (docker-outside-of-docker)", - "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-outside-of-docker", - "description": "Re-use the host docker socket, adding the Docker CLI to a container. Feature invokes a script to enable using a forwarded Docker socket within a container to run Docker commands.", - "options": { - "version": { - "type": "string", - "proposals": [ - "latest", - "none", - "20.10" - ], - "default": "latest", - "description": "Select or enter a Docker/Moby CLI version. (Availability can vary by OS version.)" - }, - "moby": { - "type": "boolean", - "default": true, - "description": "Install OSS Moby build instead of Docker CE" - }, - "mobyBuildxVersion": { - "type": "string", - "default": "latest", - "description": "Install a specific version of moby-buildx when using Moby" - }, - "dockerDashComposeVersion": { - "type": "string", - "enum": [ - "none", - "v1", - "v2" - ], - "default": "v2", - "description": "Compose version to use for docker-compose (v1 or v2 or none)" - }, - "installDockerBuildx": { - "type": "boolean", - "default": true, - "description": "Install Docker Buildx" - }, - "installDockerComposeSwitch": { - "type": "boolean", - "default": true, - "description": "Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter." - } + "id": "docker-outside-of-docker", + "version": "1.8.0", + "name": "Docker (docker-outside-of-docker)", + "documentationURL": "https://github.com/devcontainers/features/tree/main/src/docker-outside-of-docker", + "description": "Re-use the host docker socket, adding the Docker CLI to a container. Feature invokes a script to enable using a forwarded Docker socket within a container to run Docker commands.", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "none", + "20.10" + ], + "default": "latest", + "description": "Select or enter a Docker/Moby CLI version. (Availability can vary by OS version.)" }, - "entrypoint": "/usr/local/share/docker-init.sh", - "customizations": { - "vscode": { - "extensions": [ - "ms-azuretools.vscode-containers" - ], - "settings": { - "github.copilot.chat.codeGeneration.instructions": [ - { - "text": "This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using the Docker daemon on the host machine." - } - ] - } - } + "moby": { + "type": "boolean", + "default": true, + "description": "Install OSS Moby build instead of Docker CE" }, - "mounts": [ - { - "source": "/var/run/docker.sock", - "target": "/var/run/docker-host.sock", - "type": "bind" - } - ], - "securityOpt": [ - "label=disable" - ], - "installsAfter": [ - "ghcr.io/devcontainers/features/common-utils" - ], - "legacyIds": [ - "docker-from-docker" - ] + "mobyBuildxVersion": { + "type": "string", + "default": "latest", + "description": "Install a specific version of moby-buildx when using Moby" + }, + "dockerDashComposeVersion": { + "type": "string", + "enum": [ + "none", + "v1", + "v2" + ], + "default": "v2", + "description": "Compose version to use for docker-compose (v1 or v2 or none)" + }, + "installDockerBuildx": { + "type": "boolean", + "default": true, + "description": "Install Docker Buildx" + }, + "installDockerComposeSwitch": { + "type": "boolean", + "default": true, + "description": "Install Compose Switch (provided docker compose is available) which is a replacement to the Compose V1 docker-compose (python) executable. It translates the command line into Compose V2 docker compose then runs the latter." + } + }, + "entrypoint": "/usr/local/share/docker-init.sh", + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.vscode-containers" + ], + "settings": { + "github.copilot.chat.codeGeneration.instructions": [ + { + "text": "This dev container includes the Docker CLI (`docker`) pre-installed and available on the `PATH` for running and managing containers using the Docker daemon on the host machine." + } + ] + } + } + }, + "mounts": [ + { + "source": "/var/run/docker.sock", + "target": "/var/run/docker-host.sock", + "type": "bind" + } + ], + "securityOpt": [ + "label=disable" + ], + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ], + "legacyIds": [ + "docker-from-docker" + ] } diff --git a/src/docker-outside-of-docker/install.sh b/src/docker-outside-of-docker/install.sh index 74fd63530..8849e0dde 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}/_lib/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() { @@ -192,6 +182,11 @@ export DEBIAN_FRONTEND=noninteractive # Install dependencies check_packages apt-transport-https curl ca-certificates gnupg2 dirmngr wget +# Update CA certificates to ensure HTTPS connections work properly +# This is especially important for Ubuntu 24.04 (Noble) and Debian Trixie +if command -v update-ca-certificates > /dev/null 2>&1; then + update-ca-certificates +fi if ! type git > /dev/null 2>&1; then check_packages git fi diff --git a/src/go/_lib/common-setup.sh b/src/go/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/go/_lib/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/go/devcontainer-feature.json b/src/go/devcontainer-feature.json index f98e65a7f..606069da9 100644 --- a/src/go/devcontainer-feature.json +++ b/src/go/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "go", - "version": "1.3.2", + "version": "1.4.0", "name": "Go", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/go", "description": "Installs Go and common Go utilities. Auto-detects latest version and installs needed dependencies.", diff --git a/src/go/install.sh b/src/go/install.sh index 85fea5dc4..825bae28e 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}/_lib/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/_lib/common-setup.sh b/src/hugo/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/hugo/_lib/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/hugo/devcontainer-feature.json b/src/hugo/devcontainer-feature.json index 376826509..342bb1ef0 100644 --- a/src/hugo/devcontainer-feature.json +++ b/src/hugo/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "hugo", - "version": "1.1.3", + "version": "1.2.0", "name": "Hugo", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/hugo", "options": { @@ -34,6 +34,6 @@ } }, "installsAfter": [ - "ghcr.io/devcontainers/features/common-utils" + "ghcr.io/devcontainers/features/common-utils" ] } diff --git a/src/hugo/install.sh b/src/hugo/install.sh index b268e384b..4c605a473 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}/_lib/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/_lib/common-setup.sh b/src/java/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/java/_lib/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/java/devcontainer-feature.json b/src/java/devcontainer-feature.json index 4198af326..c50bc15a2 100644 --- a/src/java/devcontainer-feature.json +++ b/src/java/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "java", - "version": "1.6.3", + "version": "1.7.0", "name": "Java (via SDKMAN!)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/java", "description": "Installs Java, SDKMAN! (if not installed), and needed dependencies.", @@ -122,4 +122,4 @@ "installsAfter": [ "ghcr.io/devcontainers/features/common-utils" ] -} \ No newline at end of file +} diff --git a/src/java/install.sh b/src/java/install.sh index 62fd39462..39a2d77c6 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}/_lib/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/_lib/common-setup.sh b/src/kubectl-helm-minikube/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/kubectl-helm-minikube/_lib/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/kubectl-helm-minikube/devcontainer-feature.json b/src/kubectl-helm-minikube/devcontainer-feature.json index 410a909e4..af8dc739e 100644 --- a/src/kubectl-helm-minikube/devcontainer-feature.json +++ b/src/kubectl-helm-minikube/devcontainer-feature.json @@ -1,61 +1,61 @@ { - "id": "kubectl-helm-minikube", - "version": "1.2.2", - "name": "Kubectl, Helm, and Minikube", - "documentationURL": "https://github.com/devcontainers/features/tree/main/src/kubectl-helm-minikube", - "description": "Installs latest version of kubectl, Helm, and optionally minikube. Auto-detects latest versions and installs needed dependencies.", - "options": { - "version": { - "type": "string", - "proposals": [ - "latest", - "none", - "1.23", - "1.22", - "1.21", - "none" - ], - "default": "latest", - "description": "Select or enter a Kubernetes version to install" - }, - "helm": { - "type": "string", - "proposals": [ - "latest", - "none" - ], - "default": "latest", - "description": "Select or enter a Helm version to install" - }, - "minikube": { - "type": "string", - "proposals": [ - "latest", - "none" - ], - "default": "latest", - "description": "Select or enter a Minikube version to install" - } + "id": "kubectl-helm-minikube", + "version": "1.3.1", + "name": "Kubectl, Helm, and Minikube", + "documentationURL": "https://github.com/devcontainers/features/tree/main/src/kubectl-helm-minikube", + "description": "Installs latest version of kubectl, Helm, and optionally minikube. Auto-detects latest versions and installs needed dependencies.", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "none", + "1.23", + "1.22", + "1.21", + "none" + ], + "default": "latest", + "description": "Select or enter a Kubernetes version to install" }, - "mounts": [ - { - "source": "minikube-config", - "target": "/home/vscode/.minikube", - "type": "volume" - } - ], - "customizations": { - "vscode": { - "settings": { - "github.copilot.chat.codeGeneration.instructions": [ - { - "text": "This dev container includes kubectl, Helm, optionally minikube, and needed dependencies pre-installed and available on the `PATH`. When configuring Ingress for your Kubernetes cluster, note that by default Kubernetes will bind to a specific interface's IP rather than localhost or all interfaces. This is why you need to use the Kubernetes Node's IP when connecting - even if there's only one Node as in the case of Minikube." - } - ] - } - } + "helm": { + "type": "string", + "proposals": [ + "latest", + "none" + ], + "default": "latest", + "description": "Select or enter a Helm version to install" }, - "installsAfter": [ - "ghcr.io/devcontainers/features/common-utils" - ] + "minikube": { + "type": "string", + "proposals": [ + "latest", + "none" + ], + "default": "latest", + "description": "Select or enter a Minikube version to install" + } + }, + "mounts": [ + { + "source": "minikube-config", + "target": "/home/vscode/.minikube", + "type": "volume" + } + ], + "customizations": { + "vscode": { + "settings": { + "github.copilot.chat.codeGeneration.instructions": [ + { + "text": "This dev container includes kubectl, Helm, optionally minikube, and needed dependencies pre-installed and available on the `PATH`. When configuring Ingress for your Kubernetes cluster, note that by default Kubernetes will bind to a specific interface's IP rather than localhost or all interfaces. This is why you need to use the Kubernetes Node's IP when connecting - even if there's only one Node as in the case of Minikube." + } + ] + } + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] } diff --git a/src/kubectl-helm-minikube/install.sh b/src/kubectl-helm-minikube/install.sh index f0cf1c946..d4d947c56 100755 --- a/src/kubectl-helm-minikube/install.sh +++ b/src/kubectl-helm-minikube/install.sh @@ -12,6 +12,9 @@ set -e # Clean up rm -rf /var/lib/apt/lists/* +# Fallback version when stable.txt cannot be fetched (updated: 2026-02) +KUBECTL_FALLBACK_VERSION="v1.35.1" + KUBECTL_VERSION="${VERSION:-"latest"}" HELM_VERSION="${HELM:-"latest"}" MINIKUBE_VERSION="${MINIKUBE:-"latest"}" # latest is also valid @@ -28,22 +31,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}/_lib/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 @@ -164,7 +157,15 @@ if [ ${KUBECTL_VERSION} != "none" ]; then # Install the kubectl, verify checksum echo "Downloading kubectl..." if [ "${KUBECTL_VERSION}" = "latest" ] || [ "${KUBECTL_VERSION}" = "lts" ] || [ "${KUBECTL_VERSION}" = "current" ] || [ "${KUBECTL_VERSION}" = "stable" ]; then - KUBECTL_VERSION="$(curl -sSL https://dl.k8s.io/release/stable.txt)" + KUBECTL_VERSION="$(curl -fsSL --connect-timeout 10 --max-time 30 https://dl.k8s.io/release/stable.txt 2>/dev/null | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' || echo "")" + if [ -z "${KUBECTL_VERSION}" ]; then + echo "(!) Failed to fetch kubectl stable version from dl.k8s.io, trying alternative URL..." + KUBECTL_VERSION="$(curl -fsSL --connect-timeout 10 --max-time 30 https://storage.googleapis.com/kubernetes-release/release/stable.txt 2>/dev/null | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+' || echo "")" + fi + if [ -z "${KUBECTL_VERSION}" ]; then + echo "(!) Failed to fetch kubectl stable version from both URLs. Using fallback version ${KUBECTL_FALLBACK_VERSION}" + KUBECTL_VERSION="${KUBECTL_FALLBACK_VERSION}" + fi else find_version_from_git_tags KUBECTL_VERSION https://github.com/kubernetes/kubernetes fi @@ -174,7 +175,7 @@ if [ ${KUBECTL_VERSION} != "none" ]; then curl -sSL -o /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${architecture}/kubectl" chmod 0755 /usr/local/bin/kubectl if [ "$KUBECTL_SHA256" = "automatic" ]; then - KUBECTL_SHA256="$(curl -sSL "https://dl.k8s.io/${KUBECTL_VERSION}/bin/linux/${architecture}/kubectl.sha256")" + KUBECTL_SHA256="$(curl -sSL "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${architecture}/kubectl.sha256")" fi ([ "${KUBECTL_SHA256}" = "dev-mode" ] || (echo "${KUBECTL_SHA256} */usr/local/bin/kubectl" | sha256sum -c -)) if ! type kubectl > /dev/null 2>&1; then diff --git a/src/node/_lib/common-setup.sh b/src/node/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/node/_lib/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/node/devcontainer-feature.json b/src/node/devcontainer-feature.json index c8ceb966f..9570d556a 100644 --- a/src/node/devcontainer-feature.json +++ b/src/node/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "node", - "version": "1.7.1", + "version": "1.8.0", "name": "Node.js (via nvm), yarn and pnpm.", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/node", "description": "Installs Node.js, nvm, yarn, pnpm, and needed dependencies.", diff --git a/src/node/install.sh b/src/node/install.sh index 1d89abd0a..ac04bbed1 100755 --- a/src/node/install.sh +++ b/src/node/install.sh @@ -241,22 +241,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}/_lib/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/_lib/common-setup.sh b/src/oryx/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/oryx/_lib/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/oryx/devcontainer-feature.json b/src/oryx/devcontainer-feature.json index 860a39003..98d735b95 100644 --- a/src/oryx/devcontainer-feature.json +++ b/src/oryx/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "oryx", - "version": "1.4.1", + "version": "1.5.0", "name": "Oryx", "description": "Installs the oryx CLI", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/oryx", diff --git a/src/oryx/install.sh b/src/oryx/install.sh index cf67db6b1..47260748e 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}/_lib/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/_lib/common-setup.sh b/src/php/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/php/_lib/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/php/devcontainer-feature.json b/src/php/devcontainer-feature.json index 4db478230..5c8bf040d 100644 --- a/src/php/devcontainer-feature.json +++ b/src/php/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "php", - "version": "1.1.4", + "version": "1.2.0", "name": "PHP", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/php", "options": { diff --git a/src/php/install.sh b/src/php/install.sh index 357395e88..a71f2ad3a 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}/_lib/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/_lib/common-setup.sh b/src/python/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/python/_lib/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/python/devcontainer-feature.json b/src/python/devcontainer-feature.json index 6a4121415..d8113df03 100644 --- a/src/python/devcontainer-feature.json +++ b/src/python/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "python", - "version": "1.8.0", + "version": "1.9.0", "name": "Python", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/python", "description": "Installs the provided version of Python, as well as PIPX, and other common Python utilities. JupyterLab is conditionally installed with the python feature. Note: May require source code compilation.", @@ -93,4 +93,4 @@ "ghcr.io/devcontainers/features/common-utils", "ghcr.io/devcontainers/features/oryx" ] -} \ No newline at end of file +} diff --git a/src/python/install.sh b/src/python/install.sh index be895927d..75c18c4eb 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}/_lib/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/_lib/common-setup.sh b/src/ruby/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/ruby/_lib/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/ruby/devcontainer-feature.json b/src/ruby/devcontainer-feature.json index 661c46bf0..ea2b33f65 100644 --- a/src/ruby/devcontainer-feature.json +++ b/src/ruby/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "ruby", - "version": "1.3.2", + "version": "1.4.0", "name": "Ruby (via rvm)", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/ruby", "description": "Installs Ruby, rvm, rbenv, common Ruby utilities, and needed dependencies.", diff --git a/src/ruby/install.sh b/src/ruby/install.sh index 39cb5be03..124070952 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}/_lib/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/_lib/common-setup.sh b/src/rust/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/rust/_lib/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/rust/devcontainer-feature.json b/src/rust/devcontainer-feature.json index 88b64daed..651483518 100644 --- a/src/rust/devcontainer-feature.json +++ b/src/rust/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "rust", - "version": "1.5.0", + "version": "1.6.0", "name": "Rust", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/rust", "description": "Installs Rust, common Rust utilities, and their required dependencies", @@ -68,7 +68,7 @@ "rustfmt,clippy,rust-docs", "llvm-tools-preview,rust-src,rustfmt" ] - } + } }, "customizations": { "vscode": { diff --git a/src/rust/install.sh b/src/rust/install.sh index 99a7ba8f5..5a6410e6d 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}/_lib/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/_lib/common-setup.sh b/src/sshd/_lib/common-setup.sh new file mode 100644 index 000000000..d2ac866cf --- /dev/null +++ b/src/sshd/_lib/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/sshd/devcontainer-feature.json b/src/sshd/devcontainer-feature.json index 46c4ef302..61600917a 100644 --- a/src/sshd/devcontainer-feature.json +++ b/src/sshd/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "sshd", - "version": "1.1.0", + "version": "1.2.0", "name": "SSH server", "documentationURL": "https://github.com/devcontainers/features/tree/main/src/sshd", "description": "Adds a SSH server into a container so that you can use an external terminal, sftp, or SSHFS to interact with it.", diff --git a/src/sshd/install.sh b/src/sshd/install.sh index 9b9ddedf2..f72e1ef48 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}/_lib/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/_lib/README.md b/test/_lib/README.md new file mode 100644 index 000000000..483fd2961 --- /dev/null +++ b/test/_lib/README.md @@ -0,0 +1,31 @@ +# Common Helper Function Tests + +This directory contains tests for the common-setup.sh helper function that is deployed to each feature. + +## Structure + +The `common-setup.sh` helper script is **not** shared from a central location. Instead, it's copied into each feature's `_lib/` directory: +- `src/anaconda/_lib/common-setup.sh` +- `src/docker-in-docker/_lib/common-setup.sh` +- etc. + +This is because the devcontainer CLI packages each feature independently, and external directories are not included in the build context. + +## Running Tests + +```bash +bash test/_lib/test-common-setup.sh +``` + +The tests reference one of the feature copies (anaconda) as the source of truth for validation. + +## Updating the Helper + +If you need to update the helper function: + +1. Update the source in any feature's `_lib/common-setup.sh` +2. Copy it to all other features' `_lib/` directories +3. Run the tests to verify functionality +4. Update all affected features' versions + +Note: All copies should be kept in sync to ensure consistent behavior across features. diff --git a/test/_lib/test-common-setup.sh b/test/_lib/test-common-setup.sh new file mode 100755 index 000000000..e0ec32345 --- /dev/null +++ b/test/_lib/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 from anaconda feature as reference +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/../../src/anaconda/_lib/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 diff --git a/test/docker-outside-of-docker/docker_dash_compose_v1.sh b/test/docker-outside-of-docker/docker_dash_compose_v1.sh index d95f3cf73..4ae7a9e02 100755 --- a/test/docker-outside-of-docker/docker_dash_compose_v1.sh +++ b/test/docker-outside-of-docker/docker_dash_compose_v1.sh @@ -6,7 +6,6 @@ set -e source dev-container-features-test-lib # Definition specific tests -check "docker compose" bash -c "docker compose version | grep -E '2.[0-9]+.[0-9]+'" check "docker-compose" bash -c "docker-compose --version | grep -E '1.[0-9]+.[0-9]+'" # Report result