From e280cab8734153f4e3de26db9b151d6cf27db3eb Mon Sep 17 00:00:00 2001 From: Matt Herrick <142412263+duplocloud-matt@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:13:44 -0500 Subject: [PATCH 1/4] minor version bumps, adding support for zsh in places we append to RC scripts --- src/aws-cli/NOTES.md | 6 +++++ src/aws-cli/devcontainer-feature.json | 2 +- src/aws-cli/on-create.sh | 14 +++++++++-- src/direnv/NOTES.md | 8 ++++++ src/direnv/devcontainer-feature.json | 2 +- src/direnv/on-create.sh | 16 +++++++++--- src/gcloud-cli/NOTES.md | 8 ++++++ src/gcloud-cli/devcontainer-feature.json | 2 +- src/gcloud-cli/on-create.sh | 19 +++++++++++--- src/onepassword-cli/NOTES.md | 9 ++++++- src/onepassword-cli/devcontainer-feature.json | 2 +- src/onepassword-cli/on-create.sh | 25 +++++++++++++------ src/terraform/NOTES.md | 2 +- test/aws-cli/test.sh | 18 +++++++++++-- test/direnv/test.sh | 14 +++++++++++ test/gcloud-cli/test.sh | 14 +++++++++++ test/onepassword-cli/test.sh | 14 +++++++++++ 17 files changed, 150 insertions(+), 25 deletions(-) create mode 100644 src/direnv/NOTES.md create mode 100644 src/gcloud-cli/NOTES.md diff --git a/src/aws-cli/NOTES.md b/src/aws-cli/NOTES.md index 777ae5b..9412d81 100644 --- a/src/aws-cli/NOTES.md +++ b/src/aws-cli/NOTES.md @@ -89,6 +89,12 @@ Quick create your profile: duploctl jit update_aws_config myprofile ``` +## Shell Compatibility + +The on-create script detects the remote user's login shell (from `/etc/passwd`, falling back to +`$SHELL`) and sources the AWS CLI helpers into the matching interactive rc file — `~/.zshrc` for +zsh, `~/.bashrc` otherwise — so the helpers load on zsh-based images as well as bash. + ## References - [Duploctl JIT Documentation](https://cli.duplocloud.com/Jit/#duplo_resource.jit.DuploJit.update_aws_config) diff --git a/src/aws-cli/devcontainer-feature.json b/src/aws-cli/devcontainer-feature.json index 1c85e74..a229bf3 100644 --- a/src/aws-cli/devcontainer-feature.json +++ b/src/aws-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "AWS CLI with Custom Aliases", "id": "aws-cli", - "version": "1.0.11", + "version": "1.0.12", "description": "Installs AWS CLI with custom aliases and AWS Toolkit extension", "documentationURL": "https://github.com/duplocloud-internal/customer-workspace/tree/main/features/aws-cli", "options": { diff --git a/src/aws-cli/on-create.sh b/src/aws-cli/on-create.sh index c9acc42..dcfd4c4 100644 --- a/src/aws-cli/on-create.sh +++ b/src/aws-cli/on-create.sh @@ -6,6 +6,16 @@ echo "Configuring AWS CLI..." # Use devcontainer environment variables with fallbacks USER_HOME="${_REMOTE_USER_HOME:-$HOME}" +# Detect the remote user's login shell and pick the matching interactive rc file +USER_NAME="${_REMOTE_USER:-$(whoami)}" +LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" +LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" +case "$(basename "$LOGIN_SHELL")" in + zsh) SHELL_KIND="zsh"; SHELL_RC="${USER_HOME}/.zshrc" ;; + *) SHELL_KIND="bash"; SHELL_RC="${USER_HOME}/.bashrc" ;; +esac +touch "$SHELL_RC" + # Source feature configuration from install-time options if [[ -f /usr/local/etc/aws-cli-feature.conf ]]; then source /usr/local/etc/aws-cli-feature.conf @@ -17,8 +27,8 @@ if [[ "$JIT" == "true" ]]; then bash /usr/local/share/aws-cli-configure-jit.sh fi -# Source helpers in .bashrc -cat <> "${USER_HOME}/.bashrc" +# Source helpers in the user's rc file +cat <> "$SHELL_RC" ## Sourced from Duplocloud AWS CLI Feature if [ -f '/usr/local/share/aws-cli-helpers.sh' ]; then . '/usr/local/share/aws-cli-helpers.sh'; fi diff --git a/src/direnv/NOTES.md b/src/direnv/NOTES.md new file mode 100644 index 0000000..48da634 --- /dev/null +++ b/src/direnv/NOTES.md @@ -0,0 +1,8 @@ +## Shell Compatibility + +The on-create script detects the remote user's login shell (from `/etc/passwd`, falling back to +`$SHELL`) and writes the direnv configuration to the matching interactive rc file — `~/.zshrc` for +zsh, `~/.bashrc` otherwise. It also installs the shell-appropriate hook: `eval "$(direnv hook zsh)"` +for zsh and `eval "$(direnv hook bash)"` for bash. This makes direnv activate correctly on +zsh-based images (such as the Anthropic secure-AI reference image), where `.bashrc` is never read +and the bash hook would not load. diff --git a/src/direnv/devcontainer-feature.json b/src/direnv/devcontainer-feature.json index f1bc9b0..04b58d1 100644 --- a/src/direnv/devcontainer-feature.json +++ b/src/direnv/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "Direnv", "id": "direnv", - "version": "1.1.2", + "version": "1.1.3", "description": "Installs direnv and places a DuploCloud direnvrc at $HOME/direnv/direnvrc", "documentationURL": "https://github.com/duplocloud/devcontainers/tree/main/src/direnv", "options": {}, diff --git a/src/direnv/on-create.sh b/src/direnv/on-create.sh index e39ad2c..c9a341a 100644 --- a/src/direnv/on-create.sh +++ b/src/direnv/on-create.sh @@ -6,12 +6,22 @@ echo "Configuring direnv..." # Use devcontainer environment variables with fallbacks USER_HOME="${_REMOTE_USER_HOME:-$HOME}" -# Add direnv config and bash hook to .bashrc -cat <> "${USER_HOME}/.bashrc" +# Detect the remote user's login shell and pick the matching interactive rc file +USER_NAME="${_REMOTE_USER:-$(whoami)}" +LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" +LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" +case "$(basename "$LOGIN_SHELL")" in + zsh) SHELL_KIND="zsh"; SHELL_RC="${USER_HOME}/.zshrc" ;; + *) SHELL_KIND="bash"; SHELL_RC="${USER_HOME}/.bashrc" ;; +esac +touch "$SHELL_RC" + +# Add direnv config and the shell-appropriate hook to the user's rc file +cat <> "$SHELL_RC" ## Sourced from Duplocloud Direnv Feature export DIRENV_CONFIG="${USER_HOME}/direnv" -eval "\$(direnv hook bash)" +eval "\$(direnv hook ${SHELL_KIND})" EOF diff --git a/src/gcloud-cli/NOTES.md b/src/gcloud-cli/NOTES.md new file mode 100644 index 0000000..9d39094 --- /dev/null +++ b/src/gcloud-cli/NOTES.md @@ -0,0 +1,8 @@ +## Shell Compatibility + +The on-create script detects the remote user's login shell (from `/etc/passwd`, falling back to +`$SHELL`) and configures the matching interactive rc file — `~/.zshrc` for zsh, `~/.bashrc` +otherwise. It sources the shell-appropriate gcloud SDK include files (`path.zsh.inc` / +`completion.zsh.inc` for zsh, the `.bash.inc` variants for bash) plus the gcloud helper functions. +This makes the gcloud CLI path and completion load correctly on zsh-based images (such as the +Anthropic secure-AI reference image), where `.bashrc` is never read. diff --git a/src/gcloud-cli/devcontainer-feature.json b/src/gcloud-cli/devcontainer-feature.json index 6b58f3e..93c623f 100644 --- a/src/gcloud-cli/devcontainer-feature.json +++ b/src/gcloud-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "Google Cloud CLI", "id": "gcloud-cli", - "version": "1.0.5", + "version": "1.0.6", "description": "Installs Google Cloud CLI with multi-architecture support", "documentationURL": "https://github.com/duplocloud-internal/customer-workspace/tree/main/features/gcloud-cli", "options": {}, diff --git a/src/gcloud-cli/on-create.sh b/src/gcloud-cli/on-create.sh index abcccab..01795ed 100755 --- a/src/gcloud-cli/on-create.sh +++ b/src/gcloud-cli/on-create.sh @@ -6,15 +6,26 @@ echo "Configuring Google Cloud CLI..." # Use devcontainer environment variables with fallbacks USER_HOME="${_REMOTE_USER_HOME:-$HOME}" -# Add gcloud CLI setup to .bashrc with some extra roomy spacing -cat <> "${USER_HOME}/.bashrc" +# Detect the remote user's login shell and pick the matching interactive rc file +USER_NAME="${_REMOTE_USER:-$(whoami)}" +LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" +LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" +case "$(basename "$LOGIN_SHELL")" in + zsh) SHELL_KIND="zsh"; SHELL_RC="${USER_HOME}/.zshrc" ;; + *) SHELL_KIND="bash"; SHELL_RC="${USER_HOME}/.bashrc" ;; +esac +touch "$SHELL_RC" + +# Add gcloud CLI setup to the user's rc file with some extra roomy spacing. +# The gcloud SDK ships shell-specific include files (path.bash.inc / path.zsh.inc, etc.) +cat <> "$SHELL_RC" ## Sourced from Duplocloud GCloud CLI Feature # Google Cloud CLI - path setup -if [ -f '${USER_HOME}/google-cloud-sdk/path.bash.inc' ]; then . '${USER_HOME}/google-cloud-sdk/path.bash.inc'; fi +if [ -f '${USER_HOME}/google-cloud-sdk/path.${SHELL_KIND}.inc' ]; then . '${USER_HOME}/google-cloud-sdk/path.${SHELL_KIND}.inc'; fi # Google Cloud CLI - shell command completion -if [ -f '${USER_HOME}/google-cloud-sdk/completion.bash.inc' ]; then . '${USER_HOME}/google-cloud-sdk/completion.bash.inc'; fi +if [ -f '${USER_HOME}/google-cloud-sdk/completion.${SHELL_KIND}.inc' ]; then . '${USER_HOME}/google-cloud-sdk/completion.${SHELL_KIND}.inc'; fi # Google Cloud CLI - helper functions if [ -f '/usr/local/share/gcloud-cli-helpers.sh' ]; then . '/usr/local/share/gcloud-cli-helpers.sh'; fi diff --git a/src/onepassword-cli/NOTES.md b/src/onepassword-cli/NOTES.md index aa1a469..9f40a32 100644 --- a/src/onepassword-cli/NOTES.md +++ b/src/onepassword-cli/NOTES.md @@ -125,7 +125,7 @@ This authentication method allows biometric authentication within the devcontain - Extracted during the sign-in process using `op signin --raw` - Mapped to the correct account UUID from `op account list` - Exported to the current script environment for immediate use -- Appended to `.bashrc` for persistence across terminal sessions +- Appended to the user's login-shell rc file for persistence across terminal sessions (see [Shell Compatibility](#shell-compatibility)) **Note**: The account URL may be specified with or without the `https://` prefix. The feature handles both formats when looking up the account UUID. @@ -392,6 +392,13 @@ The install script automatically disables problematic repositories (like yarn) b This issue is specific to Debian Trixie which uses sequoia (`sqv`) for GPG verification instead of the traditional apt-key approach. +## Shell Compatibility + +The on-create script detects the remote user's login shell (from `/etc/passwd`, falling back to +`$SHELL`) and writes its persisted variables and the session-token sourcing line to the matching +interactive rc file — `~/.zshrc` for zsh, `~/.bashrc` otherwise. This makes the feature work on +zsh-based images (such as the Anthropic secure-AI reference image) where `.bashrc` is never read. + ## References - [1Password CLI Documentation](https://developer.1password.com/docs/cli) - Official CLI reference and setup guides diff --git a/src/onepassword-cli/devcontainer-feature.json b/src/onepassword-cli/devcontainer-feature.json index 31ce04a..410f146 100644 --- a/src/onepassword-cli/devcontainer-feature.json +++ b/src/onepassword-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "1Password CLI", "id": "onepassword-cli", - "version": "1.1.0", + "version": "1.1.1", "description": "Installs 1Password CLI with optional auto SSH key configuration", "documentationURL": "https://github.com/duplocloud/devcontainers/tree/main/src/onepassword-cli", "options": { diff --git a/src/onepassword-cli/on-create.sh b/src/onepassword-cli/on-create.sh index e9ad3f9..b686966 100644 --- a/src/onepassword-cli/on-create.sh +++ b/src/onepassword-cli/on-create.sh @@ -7,6 +7,15 @@ echo "Configuring 1Password CLI..." USER_HOME="${_REMOTE_USER_HOME:-$HOME}" USER_NAME="${_REMOTE_USER:-$(whoami)}" +# Detect the remote user's login shell and pick the matching interactive rc file +LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" +LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" +case "$(basename "$LOGIN_SHELL")" in + zsh) SHELL_KIND="zsh"; SHELL_RC="${USER_HOME}/.zshrc" ;; + *) SHELL_KIND="bash"; SHELL_RC="${USER_HOME}/.bashrc" ;; +esac +touch "$SHELL_RC" + # Load feature configuration if [ -f /usr/local/etc/onepassword-feature.conf ]; then source /usr/local/etc/onepassword-feature.conf @@ -85,10 +94,10 @@ function check_authentication() { # Connect requires JSON format - set globally export OP_FORMAT="json" # Persist to bashrc - local bashrc="${USER_HOME}/.bashrc" + local bashrc="$SHELL_RC" if ! grep -q "^export OP_FORMAT=" "$bashrc" 2>/dev/null; then echo 'export OP_FORMAT="json"' >> "$bashrc" - echo "OP_FORMAT=json persisted to .bashrc" + echo "OP_FORMAT=json persisted to $SHELL_RC" fi return 0 fi @@ -101,10 +110,10 @@ function check_authentication() { # Service accounts require JSON format - set globally export OP_FORMAT="json" # Persist to bashrc - local bashrc="${USER_HOME}/.bashrc" + local bashrc="$SHELL_RC" if ! grep -q "^export OP_FORMAT=" "$bashrc" 2>/dev/null; then echo 'export OP_FORMAT="json"' >> "$bashrc" - echo "OP_FORMAT=json persisted to .bashrc" + echo "OP_FORMAT=json persisted to $SHELL_RC" fi return 0 fi @@ -155,7 +164,7 @@ function check_authentication() { # Configure vault environment variable function configure_vault() { - local bashrc="${USER_HOME}/.bashrc" + local bashrc="$SHELL_RC" # Handle OP_VAULT (vault ID) - env takes precedence over vaultID option if [ -z "${OP_VAULT:-}" ]; then @@ -506,12 +515,12 @@ if [ -f /tmp/op-session.env ]; then source /tmp/op-session.env fi -# Wire .bashrc to source the ephemeral session file on every new shell (idempotent) -bashrc="${USER_HOME}/.bashrc" +# Wire the user's rc file to source the ephemeral session file on every new shell (idempotent) +bashrc="$SHELL_RC" session_env_line='[ -f /tmp/op-session.env ] && source /tmp/op-session.env' if ! grep -qF "$session_env_line" "$bashrc" 2>/dev/null; then echo "$session_env_line" >> "$bashrc" - echo "Added /tmp/op-session.env sourcing to .bashrc" + echo "Added /tmp/op-session.env sourcing to $SHELL_RC" fi if [ "$OP_AUTHENTICATED" = "true" ]; then diff --git a/src/terraform/NOTES.md b/src/terraform/NOTES.md index cd46345..415dd57 100644 --- a/src/terraform/NOTES.md +++ b/src/terraform/NOTES.md @@ -90,7 +90,7 @@ The feature installs `tf.sh` in two locations: - `/usr/local/bin/tf` - Executable wrapper (use as `tf` command) - `/usr/local/share/duplocloud/tf.sh` - Sourceable shell script (for functions) -Both files are sourced in shell rc files (`.bashrc`, `.zshrc`, `/etc/bash.bashrc`) to make the helper functions available in interactive shells. +The `tf` wrapper is placed on `PATH`, so it works in any interactive shell (bash or zsh) with no rc-file changes. If you want the underlying helper functions in your current shell, source the sharable copy directly: `source /usr/local/share/duplocloud/tf.sh`. ## References diff --git a/test/aws-cli/test.sh b/test/aws-cli/test.sh index 629fe1b..0789623 100755 --- a/test/aws-cli/test.sh +++ b/test/aws-cli/test.sh @@ -13,8 +13,22 @@ source dev-container-features-test-lib # Feature-specific tests check "aws alias file exists" test -f "${_REMOTE_USER_HOME:-$HOME}/.aws/cli/alias" check "alias file is readable" cat "${_REMOTE_USER_HOME:-$HOME}/.aws/cli/alias" -check "configure-aws-jit.sh script exists" test -f "/usr/local/bin/configure-aws-jit.sh" -check "configure-aws-jit.sh is executable" test -x "/usr/local/bin/configure-aws-jit.sh" +check "configure-aws-jit.sh script exists" test -f "/usr/local/share/aws-cli-configure-jit.sh" +check "configure-aws-jit.sh is executable" test -x "/usr/local/share/aws-cli-configure-jit.sh" + +# The on-create script must source the helpers from the login shell's rc file +# (zsh -> .zshrc, else .bashrc) so they load on zsh-based images too. +check "aws helpers sourced in login-shell rc" bash -c ' + HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" + USER_NAME="${_REMOTE_USER:-$(whoami)}" + LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" + LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" + case "$(basename "$LOGIN_SHELL")" in + zsh) RC="$HOME_DIR/.zshrc" ;; + *) RC="$HOME_DIR/.bashrc" ;; + esac + grep -q "aws-cli-helpers.sh" "$RC" +' # Report results reportResults diff --git a/test/direnv/test.sh b/test/direnv/test.sh index 26fc3e1..81b46a8 100644 --- a/test/direnv/test.sh +++ b/test/direnv/test.sh @@ -15,4 +15,18 @@ USER_HOME="${_REMOTE_USER_HOME:-$HOME}" check "direnv is installed" command -v direnv check "direnvrc installed" test -f "${USER_HOME}/direnv/direnvrc" +# The on-create script must write the direnv hook to the login shell's rc file, +# using the shell-appropriate hook (zsh -> .zshrc + 'direnv hook zsh', else .bashrc + bash). +check "direnv hook configured in login-shell rc" bash -c ' + HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" + USER_NAME="${_REMOTE_USER:-$(whoami)}" + LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" + LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" + case "$(basename "$LOGIN_SHELL")" in + zsh) RC="$HOME_DIR/.zshrc"; KIND=zsh ;; + *) RC="$HOME_DIR/.bashrc"; KIND=bash ;; + esac + grep -q "direnv hook $KIND" "$RC" +' + reportResults diff --git a/test/gcloud-cli/test.sh b/test/gcloud-cli/test.sh index aa1b0fc..aae2f35 100755 --- a/test/gcloud-cli/test.sh +++ b/test/gcloud-cli/test.sh @@ -28,5 +28,19 @@ fi; \ echo "$OUT" | grep -qi "Google Cloud SDK" \ ' +# The on-create script must source the shell-appropriate gcloud include files from the +# login shell's rc file (zsh -> .zshrc + .zsh.inc, else .bashrc + .bash.inc). +check "gcloud path inc sourced in login-shell rc" bash -c ' + HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" + USER_NAME="${_REMOTE_USER:-$(whoami)}" + LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" + LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" + case "$(basename "$LOGIN_SHELL")" in + zsh) RC="$HOME_DIR/.zshrc"; KIND=zsh ;; + *) RC="$HOME_DIR/.bashrc"; KIND=bash ;; + esac + grep -q "google-cloud-sdk/path.$KIND.inc" "$RC" +' + # Report results reportResults diff --git a/test/onepassword-cli/test.sh b/test/onepassword-cli/test.sh index 7caedd7..d63064c 100755 --- a/test/onepassword-cli/test.sh +++ b/test/onepassword-cli/test.sh @@ -33,5 +33,19 @@ check "config file exists" test -f /usr/local/etc/onepassword-feature.conf # Verify default config values check "interactive defaults to false" grep -q '^INTERACTIVE="false"$' /usr/local/etc/onepassword-feature.conf +# The on-create script wires the ephemeral session file into the login shell's rc file +# (zsh -> .zshrc, else .bashrc) so op sessions load in new shells on zsh-based images too. +check "session-env sourcing wired into login-shell rc" bash -c ' + HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" + USER_NAME="${_REMOTE_USER:-$(whoami)}" + LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" + LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" + case "$(basename "$LOGIN_SHELL")" in + zsh) RC="$HOME_DIR/.zshrc" ;; + *) RC="$HOME_DIR/.bashrc" ;; + esac + grep -qF "/tmp/op-session.env" "$RC" +' + # Report results reportResults From f8a39080ecc8d3ab9c5aafac02a2a9098302673c Mon Sep 17 00:00:00 2001 From: Matt Herrick <142412263+duplocloud-matt@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:27:23 -0500 Subject: [PATCH 2/4] converting rc scripts to use both bash and zsh for greatest possible compatibility and flexibility --- src/aws-cli/NOTES.md | 9 ++- src/aws-cli/devcontainer-feature.json | 2 +- src/aws-cli/on-create.sh | 27 ++++--- src/direnv/NOTES.md | 14 ++-- src/direnv/devcontainer-feature.json | 2 +- src/direnv/on-create.sh | 30 ++++---- src/gcloud-cli/NOTES.md | 14 ++-- src/gcloud-cli/devcontainer-feature.json | 2 +- src/gcloud-cli/on-create.sh | 34 ++++---- src/onepassword-cli/NOTES.md | 12 +-- src/onepassword-cli/devcontainer-feature.json | 2 +- src/onepassword-cli/on-create.sh | 77 +++++++++---------- src/onepassword-cli/op-session-reload.sh | 50 +++++++++++- src/onepassword-cli/post-start.sh | 6 +- test/aws-cli/test.sh | 20 +++-- test/direnv/test.sh | 23 +++--- test/gcloud-cli/test.sh | 20 +++-- test/onepassword-cli/test.sh | 20 +++-- 18 files changed, 210 insertions(+), 154 deletions(-) diff --git a/src/aws-cli/NOTES.md b/src/aws-cli/NOTES.md index 9412d81..b545306 100644 --- a/src/aws-cli/NOTES.md +++ b/src/aws-cli/NOTES.md @@ -91,9 +91,12 @@ duploctl jit update_aws_config myprofile ## Shell Compatibility -The on-create script detects the remote user's login shell (from `/etc/passwd`, falling back to -`$SHELL`) and sources the AWS CLI helpers into the matching interactive rc file — `~/.zshrc` for -zsh, `~/.bashrc` otherwise — so the helpers load on zsh-based images as well as bash. +The on-create script sources the AWS CLI helpers into **every installed shell's** interactive rc +file — `~/.bashrc` when `bash` is present and `~/.zshrc` when `zsh` is present (appends are +idempotent). It deliberately does not key off the login shell (`/etc/passwd` / `$SHELL`), because +images often install zsh as the terminal's default without changing the user's login shell, which +would leave the actually-used shell unconfigured. This way the helpers load on zsh-based images as +well as bash. ## References diff --git a/src/aws-cli/devcontainer-feature.json b/src/aws-cli/devcontainer-feature.json index a229bf3..06cfbe2 100644 --- a/src/aws-cli/devcontainer-feature.json +++ b/src/aws-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "AWS CLI with Custom Aliases", "id": "aws-cli", - "version": "1.0.12", + "version": "1.0.13", "description": "Installs AWS CLI with custom aliases and AWS Toolkit extension", "documentationURL": "https://github.com/duplocloud-internal/customer-workspace/tree/main/features/aws-cli", "options": { diff --git a/src/aws-cli/on-create.sh b/src/aws-cli/on-create.sh index dcfd4c4..91f1c97 100644 --- a/src/aws-cli/on-create.sh +++ b/src/aws-cli/on-create.sh @@ -6,16 +6,6 @@ echo "Configuring AWS CLI..." # Use devcontainer environment variables with fallbacks USER_HOME="${_REMOTE_USER_HOME:-$HOME}" -# Detect the remote user's login shell and pick the matching interactive rc file -USER_NAME="${_REMOTE_USER:-$(whoami)}" -LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" -LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" -case "$(basename "$LOGIN_SHELL")" in - zsh) SHELL_KIND="zsh"; SHELL_RC="${USER_HOME}/.zshrc" ;; - *) SHELL_KIND="bash"; SHELL_RC="${USER_HOME}/.bashrc" ;; -esac -touch "$SHELL_RC" - # Source feature configuration from install-time options if [[ -f /usr/local/etc/aws-cli-feature.conf ]]; then source /usr/local/etc/aws-cli-feature.conf @@ -27,12 +17,25 @@ if [[ "$JIT" == "true" ]]; then bash /usr/local/share/aws-cli-configure-jit.sh fi -# Source helpers in the user's rc file -cat <> "$SHELL_RC" +# Source helpers in every installed shell's interactive rc file. We can't rely on the login shell +# ($SHELL / getent passwd): images often install zsh as the terminal default without changing the +# user's login shell, so targeting a single shell leaves the actually-used shell unconfigured. +configure_shell() { + local kind="$1" rc="$2" + command -v "$kind" >/dev/null 2>&1 || return 0 + touch "$rc" + # Idempotent: skip if this feature's block is already present. + grep -qF 'Duplocloud AWS CLI Feature' "$rc" 2>/dev/null && return 0 + cat <> "$rc" ## Sourced from Duplocloud AWS CLI Feature if [ -f '/usr/local/share/aws-cli-helpers.sh' ]; then . '/usr/local/share/aws-cli-helpers.sh'; fi EOF + echo "AWS CLI helpers added to ${rc}" +} + +configure_shell bash "${USER_HOME}/.bashrc" +configure_shell zsh "${USER_HOME}/.zshrc" echo "AWS CLI configured successfully!" diff --git a/src/direnv/NOTES.md b/src/direnv/NOTES.md index 48da634..8fb27c9 100644 --- a/src/direnv/NOTES.md +++ b/src/direnv/NOTES.md @@ -1,8 +1,10 @@ ## Shell Compatibility -The on-create script detects the remote user's login shell (from `/etc/passwd`, falling back to -`$SHELL`) and writes the direnv configuration to the matching interactive rc file — `~/.zshrc` for -zsh, `~/.bashrc` otherwise. It also installs the shell-appropriate hook: `eval "$(direnv hook zsh)"` -for zsh and `eval "$(direnv hook bash)"` for bash. This makes direnv activate correctly on -zsh-based images (such as the Anthropic secure-AI reference image), where `.bashrc` is never read -and the bash hook would not load. +The on-create script installs the direnv configuration into **every installed shell's** interactive +rc file — `~/.bashrc` with `eval "$(direnv hook bash)"` when `bash` is present, and `~/.zshrc` with +`eval "$(direnv hook zsh)"` when `zsh` is present (appends are idempotent). It deliberately does not +key off the login shell (`/etc/passwd` / `$SHELL`): images frequently install zsh and make it the +terminal's default shell without changing the user's login shell, so detecting a single shell leaves +the shell the terminal actually launches without the hook. Writing to each installed shell's rc makes +direnv activate regardless of which shell opens — including zsh-based images such as the Anthropic +secure-AI reference image. diff --git a/src/direnv/devcontainer-feature.json b/src/direnv/devcontainer-feature.json index 04b58d1..1c13b61 100644 --- a/src/direnv/devcontainer-feature.json +++ b/src/direnv/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "Direnv", "id": "direnv", - "version": "1.1.3", + "version": "1.1.4", "description": "Installs direnv and places a DuploCloud direnvrc at $HOME/direnv/direnvrc", "documentationURL": "https://github.com/duplocloud/devcontainers/tree/main/src/direnv", "options": {}, diff --git a/src/direnv/on-create.sh b/src/direnv/on-create.sh index c9a341a..7437ad3 100644 --- a/src/direnv/on-create.sh +++ b/src/direnv/on-create.sh @@ -6,23 +6,27 @@ echo "Configuring direnv..." # Use devcontainer environment variables with fallbacks USER_HOME="${_REMOTE_USER_HOME:-$HOME}" -# Detect the remote user's login shell and pick the matching interactive rc file -USER_NAME="${_REMOTE_USER:-$(whoami)}" -LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" -LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" -case "$(basename "$LOGIN_SHELL")" in - zsh) SHELL_KIND="zsh"; SHELL_RC="${USER_HOME}/.zshrc" ;; - *) SHELL_KIND="bash"; SHELL_RC="${USER_HOME}/.bashrc" ;; -esac -touch "$SHELL_RC" - -# Add direnv config and the shell-appropriate hook to the user's rc file -cat <> "$SHELL_RC" +# Install the direnv hook into every installed shell's interactive rc file. +# We can't rely on the login shell ($SHELL / getent passwd): images often install zsh and set it as +# the terminal's default without changing the user's login shell, so detecting a single shell leaves +# the actually-used shell without the hook. Writing to each installed shell's rc is robust to that. +configure_shell() { + local kind="$1" rc="$2" + command -v "$kind" >/dev/null 2>&1 || return 0 + touch "$rc" + # Idempotent: skip if this feature's hook is already present. + grep -qF "direnv hook ${kind}" "$rc" 2>/dev/null && return 0 + cat <> "$rc" ## Sourced from Duplocloud Direnv Feature export DIRENV_CONFIG="${USER_HOME}/direnv" -eval "\$(direnv hook ${SHELL_KIND})" +eval "\$(direnv hook ${kind})" EOF + echo "direnv hook added to ${rc}" +} + +configure_shell bash "${USER_HOME}/.bashrc" +configure_shell zsh "${USER_HOME}/.zshrc" echo "direnv configured successfully!" diff --git a/src/gcloud-cli/NOTES.md b/src/gcloud-cli/NOTES.md index 9d39094..5e64f2f 100644 --- a/src/gcloud-cli/NOTES.md +++ b/src/gcloud-cli/NOTES.md @@ -1,8 +1,10 @@ ## Shell Compatibility -The on-create script detects the remote user's login shell (from `/etc/passwd`, falling back to -`$SHELL`) and configures the matching interactive rc file — `~/.zshrc` for zsh, `~/.bashrc` -otherwise. It sources the shell-appropriate gcloud SDK include files (`path.zsh.inc` / -`completion.zsh.inc` for zsh, the `.bash.inc` variants for bash) plus the gcloud helper functions. -This makes the gcloud CLI path and completion load correctly on zsh-based images (such as the -Anthropic secure-AI reference image), where `.bashrc` is never read. +The on-create script configures **every installed shell's** interactive rc file — `~/.bashrc` when +`bash` is present and `~/.zshrc` when `zsh` is present (appends are idempotent). Each rc sources the +shell-appropriate gcloud SDK include files (`path.bash.inc` / `completion.bash.inc` for bash, the +`.zsh.inc` variants for zsh) plus the gcloud helper functions. It deliberately does not key off the +login shell (`/etc/passwd` / `$SHELL`), because images often install zsh as the terminal's default +without changing the user's login shell, which would leave the actually-used shell unconfigured. This +makes the gcloud CLI path and completion load correctly on zsh-based images (such as the Anthropic +secure-AI reference image) as well as bash. diff --git a/src/gcloud-cli/devcontainer-feature.json b/src/gcloud-cli/devcontainer-feature.json index 93c623f..42b20c7 100644 --- a/src/gcloud-cli/devcontainer-feature.json +++ b/src/gcloud-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "Google Cloud CLI", "id": "gcloud-cli", - "version": "1.0.6", + "version": "1.0.7", "description": "Installs Google Cloud CLI with multi-architecture support", "documentationURL": "https://github.com/duplocloud-internal/customer-workspace/tree/main/features/gcloud-cli", "options": {}, diff --git a/src/gcloud-cli/on-create.sh b/src/gcloud-cli/on-create.sh index 01795ed..1d29ad4 100755 --- a/src/gcloud-cli/on-create.sh +++ b/src/gcloud-cli/on-create.sh @@ -6,30 +6,34 @@ echo "Configuring Google Cloud CLI..." # Use devcontainer environment variables with fallbacks USER_HOME="${_REMOTE_USER_HOME:-$HOME}" -# Detect the remote user's login shell and pick the matching interactive rc file -USER_NAME="${_REMOTE_USER:-$(whoami)}" -LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" -LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" -case "$(basename "$LOGIN_SHELL")" in - zsh) SHELL_KIND="zsh"; SHELL_RC="${USER_HOME}/.zshrc" ;; - *) SHELL_KIND="bash"; SHELL_RC="${USER_HOME}/.bashrc" ;; -esac -touch "$SHELL_RC" - -# Add gcloud CLI setup to the user's rc file with some extra roomy spacing. -# The gcloud SDK ships shell-specific include files (path.bash.inc / path.zsh.inc, etc.) -cat <> "$SHELL_RC" +# Add gcloud CLI setup to every installed shell's interactive rc file. The gcloud SDK ships +# shell-specific include files (path.bash.inc / path.zsh.inc, etc.), so each shell gets its own. +# We can't rely on the login shell ($SHELL / getent passwd): images often install zsh as the +# terminal default without changing the user's login shell, so targeting a single shell leaves the +# actually-used shell unconfigured. +configure_shell() { + local kind="$1" rc="$2" + command -v "$kind" >/dev/null 2>&1 || return 0 + touch "$rc" + # Idempotent: skip if this feature's block is already present. + grep -qF 'Duplocloud GCloud CLI Feature' "$rc" 2>/dev/null && return 0 + cat <> "$rc" ## Sourced from Duplocloud GCloud CLI Feature # Google Cloud CLI - path setup -if [ -f '${USER_HOME}/google-cloud-sdk/path.${SHELL_KIND}.inc' ]; then . '${USER_HOME}/google-cloud-sdk/path.${SHELL_KIND}.inc'; fi +if [ -f '${USER_HOME}/google-cloud-sdk/path.${kind}.inc' ]; then . '${USER_HOME}/google-cloud-sdk/path.${kind}.inc'; fi # Google Cloud CLI - shell command completion -if [ -f '${USER_HOME}/google-cloud-sdk/completion.${SHELL_KIND}.inc' ]; then . '${USER_HOME}/google-cloud-sdk/completion.${SHELL_KIND}.inc'; fi +if [ -f '${USER_HOME}/google-cloud-sdk/completion.${kind}.inc' ]; then . '${USER_HOME}/google-cloud-sdk/completion.${kind}.inc'; fi # Google Cloud CLI - helper functions if [ -f '/usr/local/share/gcloud-cli-helpers.sh' ]; then . '/usr/local/share/gcloud-cli-helpers.sh'; fi EOF + echo "Google Cloud CLI setup added to ${rc}" +} + +configure_shell bash "${USER_HOME}/.bashrc" +configure_shell zsh "${USER_HOME}/.zshrc" echo "Google Cloud CLI configured successfully!" diff --git a/src/onepassword-cli/NOTES.md b/src/onepassword-cli/NOTES.md index 9f40a32..5dac5e6 100644 --- a/src/onepassword-cli/NOTES.md +++ b/src/onepassword-cli/NOTES.md @@ -125,7 +125,7 @@ This authentication method allows biometric authentication within the devcontain - Extracted during the sign-in process using `op signin --raw` - Mapped to the correct account UUID from `op account list` - Exported to the current script environment for immediate use -- Appended to the user's login-shell rc file for persistence across terminal sessions (see [Shell Compatibility](#shell-compatibility)) +- Appended to every installed shell's rc file for persistence across terminal sessions (see [Shell Compatibility](#shell-compatibility)) **Note**: The account URL may be specified with or without the `https://` prefix. The feature handles both formats when looking up the account UUID. @@ -394,10 +394,12 @@ This issue is specific to Debian Trixie which uses sequoia (`sqv`) for GPG verif ## Shell Compatibility -The on-create script detects the remote user's login shell (from `/etc/passwd`, falling back to -`$SHELL`) and writes its persisted variables and the session-token sourcing line to the matching -interactive rc file — `~/.zshrc` for zsh, `~/.bashrc` otherwise. This makes the feature work on -zsh-based images (such as the Anthropic secure-AI reference image) where `.bashrc` is never read. +The on-create script writes its persisted variables and the session-token sourcing line into **every +installed shell's** interactive rc file — `~/.bashrc` when `bash` is present and `~/.zshrc` when `zsh` +is present (appends are idempotent). It deliberately does not key off the login shell (`/etc/passwd` +/ `$SHELL`), because images often install zsh as the terminal's default without changing the user's +login shell, which would leave the actually-used shell unconfigured. This makes the feature work on +zsh-based images (such as the Anthropic secure-AI reference image) as well as bash. ## References diff --git a/src/onepassword-cli/devcontainer-feature.json b/src/onepassword-cli/devcontainer-feature.json index 410f146..562c2e2 100644 --- a/src/onepassword-cli/devcontainer-feature.json +++ b/src/onepassword-cli/devcontainer-feature.json @@ -1,7 +1,7 @@ { "name": "1Password CLI", "id": "onepassword-cli", - "version": "1.1.1", + "version": "1.1.3", "description": "Installs 1Password CLI with optional auto SSH key configuration", "documentationURL": "https://github.com/duplocloud/devcontainers/tree/main/src/onepassword-cli", "options": { diff --git a/src/onepassword-cli/on-create.sh b/src/onepassword-cli/on-create.sh index b686966..3959b44 100644 --- a/src/onepassword-cli/on-create.sh +++ b/src/onepassword-cli/on-create.sh @@ -7,14 +7,25 @@ echo "Configuring 1Password CLI..." USER_HOME="${_REMOTE_USER_HOME:-$HOME}" USER_NAME="${_REMOTE_USER:-$(whoami)}" -# Detect the remote user's login shell and pick the matching interactive rc file -LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" -LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" -case "$(basename "$LOGIN_SHELL")" in - zsh) SHELL_KIND="zsh"; SHELL_RC="${USER_HOME}/.zshrc" ;; - *) SHELL_KIND="bash"; SHELL_RC="${USER_HOME}/.bashrc" ;; -esac -touch "$SHELL_RC" +# Target every installed shell's interactive rc file, not just the login shell's. Images often +# install zsh as the terminal default without changing the user's login shell, so targeting a single +# shell leaves the actually-used shell unconfigured. The lines we persist (exports + a source line) +# are shell-agnostic, so the same content applies to bash and zsh. +SHELL_RCS=() +for _s in bash zsh; do + command -v "$_s" >/dev/null 2>&1 && SHELL_RCS+=("${USER_HOME}/.${_s}rc") +done +[ ${#SHELL_RCS[@]} -eq 0 ] && SHELL_RCS=("${USER_HOME}/.bashrc") +for _rc in "${SHELL_RCS[@]}"; do touch "$_rc"; done + +# Append a line to every shell rc, only when a matching line isn't already present (idempotent). +# Usage: persist_line +persist_line() { + local pattern="$1" line="$2" rc + for rc in "${SHELL_RCS[@]}"; do + grep -q "$pattern" "$rc" 2>/dev/null || { echo "$line" >> "$rc"; echo "Persisted to $rc: $line"; } + done +} # Load feature configuration if [ -f /usr/local/etc/onepassword-feature.conf ]; then @@ -93,12 +104,7 @@ function check_authentication() { OP_AUTHENTICATED="true" # Connect requires JSON format - set globally export OP_FORMAT="json" - # Persist to bashrc - local bashrc="$SHELL_RC" - if ! grep -q "^export OP_FORMAT=" "$bashrc" 2>/dev/null; then - echo 'export OP_FORMAT="json"' >> "$bashrc" - echo "OP_FORMAT=json persisted to $SHELL_RC" - fi + persist_line "^export OP_FORMAT=" 'export OP_FORMAT="json"' return 0 fi @@ -109,12 +115,7 @@ function check_authentication() { OP_AUTHENTICATED="true" # Service accounts require JSON format - set globally export OP_FORMAT="json" - # Persist to bashrc - local bashrc="$SHELL_RC" - if ! grep -q "^export OP_FORMAT=" "$bashrc" 2>/dev/null; then - echo 'export OP_FORMAT="json"' >> "$bashrc" - echo "OP_FORMAT=json persisted to $SHELL_RC" - fi + persist_line "^export OP_FORMAT=" 'export OP_FORMAT="json"' return 0 fi @@ -164,38 +165,30 @@ function check_authentication() { # Configure vault environment variable function configure_vault() { - local bashrc="$SHELL_RC" - # Handle OP_VAULT (vault ID) - env takes precedence over vaultID option if [ -z "${OP_VAULT:-}" ]; then # No env var, check for vaultID option from feature config if [ -n "${VAULTID:-}" ]; then export OP_VAULT="$VAULTID" echo "Using vault ID from feature config: $OP_VAULT" - # Persist to bashrc - if ! grep -q "^export OP_VAULT=" "$bashrc" 2>/dev/null; then - echo "export OP_VAULT=\"${OP_VAULT}\"" >> "$bashrc" - fi + persist_line "^export OP_VAULT=" "export OP_VAULT=\"${OP_VAULT}\"" fi else echo "Using existing OP_VAULT from environment: $OP_VAULT" fi - + # Handle OP_VAULT_NAME (vault name) - env takes precedence over vault option if [ -z "${OP_VAULT_NAME:-}" ]; then # No env var, check for vault option from feature config if [ -n "${VAULT:-}" ]; then export OP_VAULT_NAME="$VAULT" echo "Using vault name from feature config: $OP_VAULT_NAME" - # Persist to bashrc - if ! grep -q "^export OP_VAULT_NAME=" "$bashrc" 2>/dev/null; then - echo "export OP_VAULT_NAME=\"${OP_VAULT_NAME}\"" >> "$bashrc" - fi + persist_line "^export OP_VAULT_NAME=" "export OP_VAULT_NAME=\"${OP_VAULT_NAME}\"" fi else echo "Using existing OP_VAULT_NAME from environment: $OP_VAULT_NAME" fi - + # Try to get vault ID from name if we have name but no ID (desktop/session only) if [ -z "${OP_VAULT:-}" ] && [ -n "${OP_VAULT_NAME:-}" ]; then if [ "$OP_AUTHENTICATED" = "true" ]; then @@ -207,10 +200,7 @@ function configure_vault() { if [ -n "$vault_id" ] && [ "$vault_id" != "null" ]; then export OP_VAULT="$vault_id" echo "Resolved vault ID: $OP_VAULT" - # Persist to bashrc - if ! grep -q "^export OP_VAULT=" "$bashrc" 2>/dev/null; then - echo "export OP_VAULT=\"${OP_VAULT}\"" >> "$bashrc" - fi + persist_line "^export OP_VAULT=" "export OP_VAULT=\"${OP_VAULT}\"" else echo "Warning: Could not resolve vault ID for '$OP_VAULT_NAME'" fi @@ -515,13 +505,16 @@ if [ -f /tmp/op-session.env ]; then source /tmp/op-session.env fi -# Wire the user's rc file to source the ephemeral session file on every new shell (idempotent) -bashrc="$SHELL_RC" +# Wire every shell's rc file to refresh the session on each new interactive shell, then source the +# ephemeral session file (idempotent). The reload runs first so the file it writes is picked up by +# the source line in the same shell. Unlike the postStart hook, an interactive shell has a TTY, so +# this is where a session sign-in (prompt or OP_PASSWD) can actually complete. The `case $-` +# guard limits it to interactive shells so scripts that source the rc don't trigger a sign-in. +reload_line='case $- in *i*) command -v op-session-reload >/dev/null 2>&1 && op-session-reload --interactive ;; esac' +persist_line "op-session-reload" "$reload_line" + session_env_line='[ -f /tmp/op-session.env ] && source /tmp/op-session.env' -if ! grep -qF "$session_env_line" "$bashrc" 2>/dev/null; then - echo "$session_env_line" >> "$bashrc" - echo "Added /tmp/op-session.env sourcing to $SHELL_RC" -fi +persist_line "op-session\.env" "$session_env_line" if [ "$OP_AUTHENTICATED" = "true" ]; then echo "1Password authenticated via: $OP_AUTH_METHOD" diff --git a/src/onepassword-cli/op-session-reload.sh b/src/onepassword-cli/op-session-reload.sh index 6234d6f..12d8314 100644 --- a/src/onepassword-cli/op-session-reload.sh +++ b/src/onepassword-cli/op-session-reload.sh @@ -37,16 +37,60 @@ if op whoami --account "$ACCOUNT" &>/dev/null 2>&1; then exit 0 fi +# Whether this invocation is allowed to prompt the user. A plain TTY test is NOT a reliable signal: +# the dev container CLI allocates a PTY for the postStart hook, so stdin/stdout look like a terminal +# even though no human is watching. Gate prompting on an EXPLICIT opt-in instead — the shell rc +# passes --interactive because a person just opened a terminal; the postStart hook never does. +ALLOW_INTERACTIVE="false" +for _arg in "$@"; do + [ "$_arg" = "--interactive" ] && ALLOW_INTERACTIVE="true" +done +[ "${OP_RELOAD_INTERACTIVE:-}" = "true" ] && ALLOW_INTERACTIVE="true" + +# True when the account is already enrolled locally (op signin then only needs a password). +account_enrolled() { + local shorthand="${ACCOUNT%%.*}" + op account list 2>/dev/null | grep -qE "(${ACCOUNT}|${shorthand})" +} + +# Sign-in requires the account to be enrolled first. Enrollment (op account add) prompts for the +# Secret Key and can't be automated, so only an explicitly-interactive invocation may do it. In the +# postStart hook the account is typically not yet enrolled — attempting it there is exactly what +# produced the hanging "add an account manually now? [Y/n]" prompt — so we skip cleanly instead. +if ! account_enrolled; then + if [ "$ALLOW_INTERACTIVE" = "true" ]; then + shorthand="${ACCOUNT%%.*}" + echo "No 1Password account enrolled; adding $ACCOUNT (shorthand: $shorthand)..." + add_cmd="op account add --address \"$ACCOUNT\" --shorthand \"$shorthand\"" + [ -n "${USEREMAIL:-}" ] && add_cmd="$add_cmd --email \"$USEREMAIL\"" + if ! eval "$add_cmd"; then + echo "Error: Failed to add 1Password account" >&2 + exit 1 + fi + else + echo "No 1Password account enrolled yet; open a terminal to sign in. Skipping." + exit 0 + fi +fi + echo "Signing in to 1Password account: $ACCOUNT" -# Get raw session token +# Get raw session token. OP_PASSWD enables a fully non-interactive sign-in (works in postStart once +# the account is enrolled). Without it we may only prompt when explicitly interactive; otherwise skip +# so the hook never blocks on a password prompt. +set +e local_passwd="${OP_PASSWD:-}" if [ -n "$local_passwd" ]; then session_token=$(echo "$local_passwd" | timeout 30 op signin --account "$ACCOUNT" --raw 2>&1) + signin_status=$? +elif [ "$ALLOW_INTERACTIVE" = "true" ]; then + session_token=$(timeout 60 op signin --account "$ACCOUNT" --raw 2>&1) + signin_status=$? else - session_token=$(timeout 15 op signin --account "$ACCOUNT" --raw 2>&1) + echo "No OP_PASSWD set and not an interactive invocation; open a terminal to sign in. Skipping." + exit 0 fi -signin_status=$? +set -e if [ $signin_status -ne 0 ]; then echo "Error: Failed to sign in to 1Password" >&2 diff --git a/src/onepassword-cli/post-start.sh b/src/onepassword-cli/post-start.sh index da5cd84..7a67fe9 100644 --- a/src/onepassword-cli/post-start.sh +++ b/src/onepassword-cli/post-start.sh @@ -11,5 +11,9 @@ if [ "${OP_ENABLED:-true}" = "false" ]; then exit 0 fi +# Refresh non-interactively (no --interactive flag): connect/service-account/desktop refresh, or an +# OP_PASSWD sign-in once the account is enrolled. Anything needing a human (enrollment, a password +# prompt) is deferred to the first interactive shell. Never let a refresh hiccup block container +# start — op-session-reload exits 0 on the deferral paths, and we tolerate genuine failures here. echo "Refreshing 1Password session..." -op-session-reload +op-session-reload || echo "1Password session not refreshed now; sign in from a terminal with: op-session-reload --interactive" diff --git a/test/aws-cli/test.sh b/test/aws-cli/test.sh index 0789623..9216917 100755 --- a/test/aws-cli/test.sh +++ b/test/aws-cli/test.sh @@ -16,18 +16,16 @@ check "alias file is readable" cat "${_REMOTE_USER_HOME:-$HOME}/.aws/cli/alias" check "configure-aws-jit.sh script exists" test -f "/usr/local/share/aws-cli-configure-jit.sh" check "configure-aws-jit.sh is executable" test -x "/usr/local/share/aws-cli-configure-jit.sh" -# The on-create script must source the helpers from the login shell's rc file -# (zsh -> .zshrc, else .bashrc) so they load on zsh-based images too. -check "aws helpers sourced in login-shell rc" bash -c ' +# The on-create script must source the helpers from every installed shell's rc file +# (bash -> .bashrc, zsh -> .zshrc) so they load regardless of which shell the terminal launches. +check "aws helpers sourced in bash rc" bash -c ' HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" - USER_NAME="${_REMOTE_USER:-$(whoami)}" - LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" - LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" - case "$(basename "$LOGIN_SHELL")" in - zsh) RC="$HOME_DIR/.zshrc" ;; - *) RC="$HOME_DIR/.bashrc" ;; - esac - grep -q "aws-cli-helpers.sh" "$RC" + grep -q "aws-cli-helpers.sh" "$HOME_DIR/.bashrc" +' +check "aws helpers sourced in zsh rc when zsh is installed" bash -c ' + HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" + command -v zsh >/dev/null 2>&1 || exit 0 + grep -q "aws-cli-helpers.sh" "$HOME_DIR/.zshrc" ' # Report results diff --git a/test/direnv/test.sh b/test/direnv/test.sh index 81b46a8..92fb5fe 100644 --- a/test/direnv/test.sh +++ b/test/direnv/test.sh @@ -15,18 +15,19 @@ USER_HOME="${_REMOTE_USER_HOME:-$HOME}" check "direnv is installed" command -v direnv check "direnvrc installed" test -f "${USER_HOME}/direnv/direnvrc" -# The on-create script must write the direnv hook to the login shell's rc file, -# using the shell-appropriate hook (zsh -> .zshrc + 'direnv hook zsh', else .bashrc + bash). -check "direnv hook configured in login-shell rc" bash -c ' +# The on-create script must write the shell-appropriate direnv hook to the rc file of every +# installed shell (bash -> .bashrc + 'direnv hook bash', zsh -> .zshrc + 'direnv hook zsh'), +# so direnv activates regardless of which shell the terminal launches. +check "direnv hook configured for bash" bash -c ' HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" - USER_NAME="${_REMOTE_USER:-$(whoami)}" - LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" - LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" - case "$(basename "$LOGIN_SHELL")" in - zsh) RC="$HOME_DIR/.zshrc"; KIND=zsh ;; - *) RC="$HOME_DIR/.bashrc"; KIND=bash ;; - esac - grep -q "direnv hook $KIND" "$RC" + grep -q "direnv hook bash" "$HOME_DIR/.bashrc" +' + +# zsh is not in every base image; only assert when it is installed. +check "direnv hook configured for zsh when zsh is installed" bash -c ' + HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" + command -v zsh >/dev/null 2>&1 || exit 0 + grep -q "direnv hook zsh" "$HOME_DIR/.zshrc" ' reportResults diff --git a/test/gcloud-cli/test.sh b/test/gcloud-cli/test.sh index aae2f35..9963511 100755 --- a/test/gcloud-cli/test.sh +++ b/test/gcloud-cli/test.sh @@ -28,18 +28,16 @@ fi; \ echo "$OUT" | grep -qi "Google Cloud SDK" \ ' -# The on-create script must source the shell-appropriate gcloud include files from the -# login shell's rc file (zsh -> .zshrc + .zsh.inc, else .bashrc + .bash.inc). -check "gcloud path inc sourced in login-shell rc" bash -c ' +# The on-create script must source the shell-appropriate gcloud include files from every installed +# shell's rc file (bash -> .bashrc + path.bash.inc, zsh -> .zshrc + path.zsh.inc). +check "gcloud path inc sourced in bash rc" bash -c ' HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" - USER_NAME="${_REMOTE_USER:-$(whoami)}" - LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" - LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" - case "$(basename "$LOGIN_SHELL")" in - zsh) RC="$HOME_DIR/.zshrc"; KIND=zsh ;; - *) RC="$HOME_DIR/.bashrc"; KIND=bash ;; - esac - grep -q "google-cloud-sdk/path.$KIND.inc" "$RC" + grep -q "google-cloud-sdk/path.bash.inc" "$HOME_DIR/.bashrc" +' +check "gcloud path inc sourced in zsh rc when zsh is installed" bash -c ' + HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" + command -v zsh >/dev/null 2>&1 || exit 0 + grep -q "google-cloud-sdk/path.zsh.inc" "$HOME_DIR/.zshrc" ' # Report results diff --git a/test/onepassword-cli/test.sh b/test/onepassword-cli/test.sh index d63064c..942794f 100755 --- a/test/onepassword-cli/test.sh +++ b/test/onepassword-cli/test.sh @@ -33,18 +33,16 @@ check "config file exists" test -f /usr/local/etc/onepassword-feature.conf # Verify default config values check "interactive defaults to false" grep -q '^INTERACTIVE="false"$' /usr/local/etc/onepassword-feature.conf -# The on-create script wires the ephemeral session file into the login shell's rc file -# (zsh -> .zshrc, else .bashrc) so op sessions load in new shells on zsh-based images too. -check "session-env sourcing wired into login-shell rc" bash -c ' +# The on-create script wires the ephemeral session file into every installed shell's rc file +# (bash -> .bashrc, zsh -> .zshrc) so op sessions load in new shells whichever shell launches. +check "session-env sourcing wired into bash rc" bash -c ' HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" - USER_NAME="${_REMOTE_USER:-$(whoami)}" - LOGIN_SHELL="$(getent passwd "$USER_NAME" 2>/dev/null | cut -d: -f7)" - LOGIN_SHELL="${LOGIN_SHELL:-${SHELL:-/bin/bash}}" - case "$(basename "$LOGIN_SHELL")" in - zsh) RC="$HOME_DIR/.zshrc" ;; - *) RC="$HOME_DIR/.bashrc" ;; - esac - grep -qF "/tmp/op-session.env" "$RC" + grep -qF "/tmp/op-session.env" "$HOME_DIR/.bashrc" +' +check "session-env sourcing wired into zsh rc when zsh is installed" bash -c ' + HOME_DIR="${_REMOTE_USER_HOME:-$HOME}" + command -v zsh >/dev/null 2>&1 || exit 0 + grep -qF "/tmp/op-session.env" "$HOME_DIR/.zshrc" ' # Report results From c2c0a8e82d26b2ec000ee5c0f0dcbc635008b5f5 Mon Sep 17 00:00:00 2001 From: Matt Herrick <142412263+duplocloud-matt@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:41:52 -0500 Subject: [PATCH 3/4] minor readme cleanup --- src/onepassword-cli/op-session-reload.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/onepassword-cli/op-session-reload.sh b/src/onepassword-cli/op-session-reload.sh index 12d8314..f618557 100644 --- a/src/onepassword-cli/op-session-reload.sh +++ b/src/onepassword-cli/op-session-reload.sh @@ -55,8 +55,7 @@ account_enrolled() { # Sign-in requires the account to be enrolled first. Enrollment (op account add) prompts for the # Secret Key and can't be automated, so only an explicitly-interactive invocation may do it. In the -# postStart hook the account is typically not yet enrolled — attempting it there is exactly what -# produced the hanging "add an account manually now? [Y/n]" prompt — so we skip cleanly instead. +# postStart hook the account is typically not yet enrolled if ! account_enrolled; then if [ "$ALLOW_INTERACTIVE" = "true" ]; then shorthand="${ACCOUNT%%.*}" From 0c9fae250fb84ea757f93e82a7263fda5dfbbf27 Mon Sep 17 00:00:00 2001 From: Matt Herrick <142412263+duplocloud-matt@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:36:00 -0500 Subject: [PATCH 4/4] onepassword-cli: drop broken add_account enrollment from on-create add_account could never enroll an account: `op account add` requires the Secret Key (1Password exposes no flag/env for it), and it ran with ` --- src/onepassword-cli/on-create.sh | 66 +++--------------------- src/onepassword-cli/op-session-reload.sh | 2 + 2 files changed, 10 insertions(+), 58 deletions(-) diff --git a/src/onepassword-cli/on-create.sh b/src/onepassword-cli/on-create.sh index 3959b44..24a1850 100644 --- a/src/onepassword-cli/on-create.sh +++ b/src/onepassword-cli/on-create.sh @@ -45,15 +45,6 @@ chmod 700 "${USER_HOME}/.ssh" # Track authentication status OP_AUTHENTICATED="false" OP_AUTH_METHOD="" -OP_ACCOUNT_SHORTHAND="" - -# Extract subdomain from account URL for shorthand -function get_account_shorthand() { - # Check if OP_ACCOUNT is set in environment first (takes precedence) - local account="${OP_ACCOUNT:-${ACCOUNT:-my.1password.com}}" - # Extract subdomain (everything before the first dot) - echo "${account%%.*}" -} # Check if terminal is interactive function is_terminal_interactive() { @@ -61,40 +52,6 @@ function is_terminal_interactive() { [ -t 0 ] && [ -t 1 ] } -# Check if account exists in op account list -function account_exists() { - local account="$1" - local shorthand="$2" - - # Check by URL or shorthand - op account list 2>/dev/null | grep -qE "(${account}|${shorthand})" -} - -# Add 1Password account -function add_account() { - local account="${ACCOUNT:-my.1password.com}" - local shorthand="$1" - local email="${USEREMAIL:-}" - - echo "Adding 1Password account: $account (shorthand: $shorthand)" - - local cmd="op account add --address '$account' --shorthand '$shorthand'" - - if [ -n "$email" ]; then - cmd="$cmd --email '$email'" - fi - - # Use timeout to prevent hanging when no one is at the terminal - if timeout 15 bash -c "$cmd" /dev/null; then OP_AUTH_METHOD="session" OP_AUTHENTICATED="true" return 0 diff --git a/src/onepassword-cli/op-session-reload.sh b/src/onepassword-cli/op-session-reload.sh index f618557..5c1b2fb 100644 --- a/src/onepassword-cli/op-session-reload.sh +++ b/src/onepassword-cli/op-session-reload.sh @@ -80,9 +80,11 @@ echo "Signing in to 1Password account: $ACCOUNT" set +e local_passwd="${OP_PASSWD:-}" if [ -n "$local_passwd" ]; then + echo "OP Password found, running op signin with it" session_token=$(echo "$local_passwd" | timeout 30 op signin --account "$ACCOUNT" --raw 2>&1) signin_status=$? elif [ "$ALLOW_INTERACTIVE" = "true" ]; then + echo "No OP_PASSWD set; prompting for interactive sign-in (up to 60s)" session_token=$(timeout 60 op signin --account "$ACCOUNT" --raw 2>&1) signin_status=$? else