From c3732fe50555f0b918bed3f7e27028e30b96e0db Mon Sep 17 00:00:00 2001 From: Chris Green Date: Wed, 25 Mar 2026 13:05:22 -0500 Subject: [PATCH] devcontainer: overhaul for rootless Podman on remote SSH hosts - Switch remoteUser to root: in rootless Podman the container root maps to the host user, eliminating UID/GID conflicts and bind-mount permission issues - Remove pinned USER_UID/USER_GID and vscode user setup from Dockerfile; install podman and socat instead; source /entrypoint.sh from root .bashrc - Set userEnvProbe: none to avoid VS Code heredoc env-JSON stall - Establish a matching-paths Podman socket proxy via socat in ensure-repos.sh (${HOME}/.podman-proxy/podman.sock); bind-mount the same path into the container and set DOCKER_HOST/CONTAINER_HOST so act and other nested-container tools work correctly - Mount host ~/.gnupg to /root/.gnupg and set GNUPGHOME; create /run/user/0 with 700 permissions for GPG agent sockets - Mount host ~/.config/gh and set GH_CONFIG_DIR for GitHub CLI access - Add post-create.sh for one-time container initialization steps - Update codespace.code-workspace and dev/jules/prepare-environment.sh Co-authored-by: greenc-FNAL <2372949+greenc-FNAL@users.noreply.github.com> --- .devcontainer/Dockerfile | 29 ++++++----- .devcontainer/codespace.code-workspace | 7 ++- .devcontainer/devcontainer.json | 25 +++++----- .devcontainer/ensure-repos.sh | 69 ++++++++++++++++++++++---- .devcontainer/post-create.sh | 19 +++++++ dev/jules/prepare-environment.sh | 32 ++++++------ 6 files changed, 125 insertions(+), 56 deletions(-) create mode 100755 .devcontainer/post-create.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c3f20ad89..f02c57d4c 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,10 +1,5 @@ FROM ghcr.io/framework-r-d/phlex-dev:latest -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=1000 -ARG SPACK_GID=2000 - # Validate Python site-packages symlink RUN <<'VALIDATE_PYTHON_SYMLINK' set -euo pipefail @@ -61,12 +56,20 @@ fi echo "Python symlink validated: $python_link -> $(readlink "$python_link")" VALIDATE_PYTHON_SYMLINK -# Create the user and add to spack group -RUN groupadd --gid $USER_GID $USERNAME \ - && useradd --uid $USER_UID --gid $USER_GID --create-home $USERNAME \ - && usermod -aG $SPACK_GID $USERNAME \ - && echo "$USERNAME ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \ - && chmod 0440 /etc/sudoers.d/$USERNAME +# Install podman client and socat inside the container to support nested +# container communication and provide a 'docker' command alias. +RUN <<'INSTALL_PODMAN_CLIENT' +set -euo pipefail +apt-get update +apt-get install -y --no-install-recommends podman socat +apt-get clean +rm -rf /var/lib/apt/lists/* +INSTALL_PODMAN_CLIENT + +# Wire up the root user's .bashrc to source the Spack environment via +# /entrypoint.sh on every interactive shell. +RUN printf '. /entrypoint.sh\n' >> /root/.bashrc -# Setup entrypoint usage in bashrc -RUN echo '. /entrypoint.sh' >> /home/$USERNAME/.bashrc +# Ensure /run/user/0 exists and is owned by root with 700 permissions to +# allow GPG agent to function correctly when running as root. +RUN mkdir -p /run/user/0 && chmod 700 /run/user/0 && chown root:root /run/user/0 diff --git a/.devcontainer/codespace.code-workspace b/.devcontainer/codespace.code-workspace index ad184f1d5..404083835 100644 --- a/.devcontainer/codespace.code-workspace +++ b/.devcontainer/codespace.code-workspace @@ -58,6 +58,7 @@ "**/build/CMakeFiles/**": true }, "python.languageServer": "Pylance", + "python.terminal.activateEnvironment": false, "python.defaultInterpreterPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", "python.analysis.typeCheckingMode": "basic", "python.analysis.diagnosticMode": "workspace", @@ -97,10 +98,11 @@ "ms-vscode.cpptools-extension-pack", "ms-vscode.hexeditor", "ms-vscode.vscode-json", - "redhat.vscode-yaml", - "twxs.cmake" + "redhat.vscode-yaml" ], "unwantedRecommendations": [ + "amazonwebservices.amazon-q-vscode", + "amazonwebservices.aws-toolkit-vscode", "github.copilot", "ikuyadeu.r", "ms-python.flake8", @@ -110,6 +112,7 @@ "ms-vscode.r-debugger", "redhat.java", "reditorsupport.r", + "twxs.cmake", "vscjava.vscode-gradle", "vscjava.vscode-java-debug", "vscjava.vscode-java-pack", diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index bad9064d3..5ffdf17f8 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,22 +7,27 @@ ] }, "workspaceFolder": "/workspaces/phlex", - "remoteUser": "vscode", + "remoteUser": "root", + "userEnvProbe": "none", "containerEnv": { - "GH_CONFIG_DIR": "/home/vscode/.config/gh", - "DOCKER_HOST": "unix:///run/podman/podman.sock" + "GH_CONFIG_DIR": "/root/.config/gh", + "DOCKER_HOST": "unix:///tmp/podman.sock", + "CONTAINER_HOST": "unix:///tmp/podman.sock", + "GNUPGHOME": "/root/.gnupg" }, "mounts": [ "source=${localWorkspaceFolder}/../phlex-design,target=/workspaces/phlex-design,type=bind", "source=${localWorkspaceFolder}/../phlex-examples,target=/workspaces/phlex-examples,type=bind", "source=${localWorkspaceFolder}/../phlex-coding-guidelines,target=/workspaces/phlex-coding-guidelines,type=bind", "source=${localWorkspaceFolder}/../phlex-spack-recipes,target=/workspaces/phlex-spack-recipes,type=bind", - "source=${localEnv:XDG_RUNTIME_DIR}/podman,target=/run/podman,type=bind", - "source=${localEnv:HOME}/.config/gh,target=/home/vscode/.config/gh,type=bind,readonly" + "source=${localEnv:HOME}/.podman-proxy/podman.sock,target=/tmp/podman.sock,type=bind", + "source=${localEnv:HOME}/.aws,target=/root/.aws,type=bind", + "source=${localEnv:HOME}/.config/gh,target=/root/.config/gh,type=bind,readonly", + "source=${localEnv:HOME}/.gnupg,target=/root/.gnupg,type=bind" ], "initializeCommand": "bash .devcontainer/ensure-repos.sh", "onCreateCommand": "bash .devcontainer/setup-repos.sh /workspaces", - "postCreateCommand": "bash -lc 'if command -v prek >/dev/null 2>&1; then prek install || true; elif command -v pre-commit >/dev/null 2>&1; then pre-commit install || true; fi'", + "postCreateCommand": "bash -lc 'bash .devcontainer/post-create.sh'", "customizations": { "vscode": { "settings": { @@ -37,13 +42,14 @@ } }, "terminal.integrated.shellIntegration.suggestEnablement": false, + "python.terminal.activateEnvironment": false, "python.defaultInterpreterPath": "/opt/spack-environments/phlex-ci/.spack-env/view/bin/python", "python.analysis.extraPaths": [ "${workspaceFolder}/build", "/opt/spack-environments/phlex-ci/.spack-env/view/lib/root", "/opt/spack-environments/phlex-ci/.spack-env/view/lib/python/site-packages" ], - "cmake.cmakePath": "${workspaceFolder}/.devcontainer/cmake_wrapper.sh", + "cmake.defaultConfigurePreset": "default", "cmake.ctestPath": "${workspaceFolder}/.devcontainer/ctest_wrapper.sh", "cmake.generator": "Ninja", "C_Cpp.default.cStandard": "c17", @@ -58,7 +64,6 @@ "charliermarsh.ruff", "cheshirekow.cmake-format", "chrisjsewell.myst-tml-syntax", - "davidanson.vscode-markdownlint", "donjayamanne.githistory", "dotjoshjohnson.xml", @@ -71,11 +76,9 @@ "lextudio.restructuredtext-pack", "lfs.vscode-emacs-friendly", "links-req-tracer.links-requirement-tracer", - "ms-python.debugpy", "ms-python.mypy-type-checker", "ms-python.pylint", - "ms-python.vscode-pylance", "ms-python.vscode-python-envs", "ms-vscode.cmake-tools", @@ -87,11 +90,9 @@ "ms-vscode.makefile-tools", "ms-vscode.vscode-websearchforcopilot", "redhat.vscode-yaml", - "shd101wyy.markdown-preview-enhanced", "swyddfa.esbonio", "trond-snekvik.simple-rst", - "twxs.cmake", "vadimcn.vscode-lldb", "wequick.coverage-gutters", "xaver.clang-format" diff --git a/.devcontainer/ensure-repos.sh b/.devcontainer/ensure-repos.sh index ccff1b125..b5bba2f33 100755 --- a/.devcontainer/ensure-repos.sh +++ b/.devcontainer/ensure-repos.sh @@ -56,17 +56,64 @@ clone_if_absent phlex-examples clone_if_absent phlex-coding-guidelines clone_if_absent phlex-spack-recipes -# Ensure the Podman runtime directory exists so the devcontainer bind mount -# has a valid source even when the Podman socket is not yet active. The -# socket itself is created by Podman at runtime; we only need the parent -# directory to exist for the mount to succeed. -PODMAN_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/podman" -if mkdir -p "${PODMAN_RUNTIME_DIR}" 2>/dev/null; then - if [ ! -S "${PODMAN_RUNTIME_DIR}/podman.sock" ]; then - echo "NOTE: Podman socket not found at ${PODMAN_RUNTIME_DIR}/podman.sock" >&2 - echo " To use 'act' inside the devcontainer, enable the Podman socket:" >&2 - echo " systemctl --user enable --now podman.socket" >&2 +# --- Podman Socket Proxy for Nested Containers (act) --- +# +# Rootless Podman nested volume mounts (like act's Docker SDK mounting the +# Podman socket) require that the source and target paths match across the +# host/container boundary. We use 'socat' on the host to provide a proxy socket +# at a stable, matching path in the user's home directory. + +USER_ID=$(id -u) +PODMAN_REAL_SOCKET="${XDG_RUNTIME_DIR:-/run/user/${USER_ID}}/podman/podman.sock" +PROXY_DIR="${HOME}/.podman-proxy" +PROXY_SOCKET="${PROXY_DIR}/podman.sock" + +# Kill any existing socat proxy for this socket +pkill -f "socat UNIX-LISTEN:${PROXY_SOCKET}" || true +# Wait for old process to die and socket to be removed +sleep 0.2 +mkdir -p "${PROXY_DIR}" +chmod 700 "${PROXY_DIR}" + +mkdir -p "${PROXY_DIR}" +chmod 700 "${PROXY_DIR}" + +if [ -S "${PODMAN_REAL_SOCKET}" ]; then + if command -v socat >/dev/null 2>&1; then + echo "Proxying Podman socket ${PODMAN_REAL_SOCKET} -> ${PROXY_SOCKET} ..." + # Use a background socat to proxy the real socket to the proxy socket. + # This socket will be bind-mounted at the SAME PATH in the container. + nohup setsid socat "UNIX-LISTEN:${PROXY_SOCKET},fork,reuseaddr,unlink-early" "UNIX-CONNECT:${PODMAN_REAL_SOCKET}" > /tmp/socat-podman.log 2>&1 & + # Wait for socket to be created + i=0 + while [ $i -lt 20 ] && [ ! -S "${PROXY_SOCKET}" ]; do + sleep 0.1 + i=$((i + 1)) + done + if [ ! -S "${PROXY_SOCKET}" ]; then + echo "WARNING: Socket creation timed out; creating placeholder" >&2 + socat "UNIX-LISTEN:${PROXY_SOCKET},fork,reuseaddr" "UNIX-CONNECT:${PODMAN_REAL_SOCKET}" & + fi + else + echo "WARNING: socat not found on host; act will not work inside the container" >&2 fi else - echo "WARNING: unable to create ${PODMAN_RUNTIME_DIR}; 'act' will not work inside the container without the Podman socket" >&2 + echo "WARNING: Podman socket not found at ${PODMAN_REAL_SOCKET}" >&2 + echo " To use 'act' inside the devcontainer, enable the Podman socket:" >&2 + echo " systemctl --user enable --now podman.socket" >&2 +fi + +# Ensure the bind-mount source always exists so 'podman run' never fails with +# "no such file or directory", even when socat is unavailable or the real +# Podman socket is absent. +if [ ! -S "${PROXY_SOCKET}" ]; then + # Create a dummy socket so the mount source is valid. The container will + # start successfully; nested container support (act) simply won't work. + python3 -c " +import socket, os +s = socket.socket(socket.AF_UNIX) +s.bind('${PROXY_SOCKET}') +" 2>/dev/null || touch "${PROXY_SOCKET}" fi + +echo "SUCCESS: .devcontainer/ensure-repos.sh completed successfully" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 000000000..56327edc3 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Run inside the dev container after creation. + +set -euo pipefail + +# Configure act to use the Podman socket and run privileged so that nested +# container operations (e.g. workflow_dispatch testing) work correctly. +cat > ~/.actrc <<'EOF' +--container-daemon-socket unix:///tmp/podman.sock +--container-options --privileged +--container-options --userns=keep-id +EOF + +# Install pre-commit hooks if available. +if command -v prek >/dev/null 2>&1; then + prek install || true +elif command -v pre-commit >/dev/null 2>&1; then + pre-commit install || true +fi diff --git a/dev/jules/prepare-environment.sh b/dev/jules/prepare-environment.sh index 140aaa218..f7b4c7361 100755 --- a/dev/jules/prepare-environment.sh +++ b/dev/jules/prepare-environment.sh @@ -183,14 +183,10 @@ git clone https://github.com/spack/spack.git "$SPACK_ROOT" sudo env INSTALLER_NO_MODIFY_PATH=1 UV_INSTALL_DIR=/usr/local/bin sh "${_uv_installer}" rm -f "${_uv_installer}" - sudo env UV_TOOL_BIN_DIR=/usr/local/bin UV_TOOL_DIR=/usr/local/share/uv/tools \ - /usr/local/bin/uv tool install ruff - sudo env UV_TOOL_BIN_DIR=/usr/local/bin UV_TOOL_DIR=/usr/local/share/uv/tools \ - /usr/local/bin/uv tool install gersemi - sudo env UV_TOOL_BIN_DIR=/usr/local/bin UV_TOOL_DIR=/usr/local/share/uv/tools \ - /usr/local/bin/uv tool install prek - sudo env UV_TOOL_BIN_DIR=/usr/local/bin UV_TOOL_DIR=/usr/local/share/uv/tools \ - /usr/local/bin/uv tool install jsonnet + for tool in ruff gersemi prek; do + sudo env UV_TOOL_BIN_DIR=/usr/local/bin UV_TOOL_DIR=/usr/local/share/uv/tools \ + /usr/local/bin/uv tool install "$tool" + done sudo /usr/local/bin/uv cache clean # Clean all Spack caches @@ -204,13 +200,11 @@ git clone https://github.com/spack/spack.git "$SPACK_ROOT" { set +x; } >/dev/null 2>&1 echo "--> Spack and Python tools setup complete." -echo "--> Installing additional developer tools (act, gh)..." +echo "--> Installing act..." set -x -# Install GitHub's act CLI download_url=$(curl -s https://api.github.com/repos/nektos/act/releases/latest | \ - grep -Ee '"browser_download_url": .*/act_Linux_x86_64\.tar\.gz"' | \ - head -n1 | \ + grep -Ee '"browser_download_url": .*/act_Linux_x86_64\.' | \ cut -d '"' -f 4) if [ -z "$download_url" ]; then echo "Failed to determine act download URL" >&2 @@ -219,16 +213,18 @@ fi curl -sfSL "$download_url" | sudo tar -C /usr/local/bin -zx act sudo chmod +x /usr/local/bin/act -# Install GitHub CLI (gh) -curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg +{ set +x; } >/dev/null 2>&1 +echo "--> Installing gh..." +set -x + +curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | \ + sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg -echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null +echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | \ + sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null sudo apt-get update sudo apt-get install -y --no-install-recommends gh -{ set +x; } >/dev/null 2>&1 -echo "--> Additional developer tools installed successfully." - echo "--> Performing final cleanup..." set -x