feat: add 1password module under bpmct namespace (#824)

Adds a 1Password module under the `bpmct` namespace.

## What it does

Installs the [1Password CLI](https://developer.1password.com/docs/cli/)
(`op`) into Coder workspaces at startup. Two auth paths:

- **Service account token** — set `service_account_token` and
`OP_SERVICE_ACCOUNT_TOKEN` is injected automatically. Fully headless.
- **Personal account** — set `account_address`, `account_email`,
`account_secret_key` to pre-register the account. User runs `op signin`
in their terminal.

Optionally installs the [1Password VS Code
extension](https://marketplace.visualstudio.com/items?itemName=1Password.op-vscode)
(`1Password.op-vscode`) for code-server and VS Code with
`install_vscode_extension = true`.

Supports `pre_install_script` and `post_install_script` for custom
orchestration.

## What's included

- `registry/bpmct/` — new namespace (Ben Potter, community)
- `registry/bpmct/modules/1password/` — the module (`main.tf`, `run.sh`,
`README.md`)
- `.icons/1password.svg` — 1Password logo from Simple Icons

## Tested

Spun up a dev Coder instance, pushed the template with a real 1Password
service account token, created a workspace, and confirmed:

- `op` CLI installs and authenticates
- `op vault list` returns vaults
- `1Password.op-vscode` extension installs in code-server

---------

Co-authored-by: DevCats <christofer@coder.com>
This commit is contained in:
Ben Potter 2026-04-01 13:38:27 -05:00 committed by GitHub
parent caaff0c1e9
commit 5bc668aa4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 395 additions and 0 deletions

1
.icons/1password.svg Normal file
View File

@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>1Password</title><path d="M12 .007C5.373.007 0 5.376 0 11.999c0 6.624 5.373 11.994 12 11.994S24 18.623 24 12C24 5.376 18.627.007 12 .007Zm-.895 4.857h1.788c.484 0 .729.002.914.096a.86.86 0 0 1 .377.377c.094.185.095.428.095.912v6.016c0 .12 0 .182-.015.238a.427.427 0 0 1-.067.137.923.923 0 0 1-.174.162l-.695.564c-.113.092-.17.138-.191.194a.216.216 0 0 0 0 .15c.02.055.078.101.191.193l.695.565c.094.076.14.115.174.162.03.042.053.087.067.137a.936.936 0 0 1 .015.238v2.746c0 .484-.001.727-.095.912a.86.86 0 0 1-.377.377c-.185.094-.43.096-.914.096h-1.788c-.484 0-.726-.002-.912-.096a.86.86 0 0 1-.377-.377c-.094-.185-.095-.428-.095-.912v-6.016c0-.12 0-.182.015-.238a.437.437 0 0 1 .067-.139c.034-.047.08-.083.174-.16l.695-.564c.113-.092.17-.138.191-.194a.216.216 0 0 0 0-.15c-.02-.055-.078-.101-.191-.193l-.695-.565a.92.92 0 0 1-.174-.162.437.437 0 0 1-.067-.139.92.92 0 0 1-.015-.236V6.25c0-.484.001-.727.095-.912a.86.86 0 0 1 .377-.377c.186-.094.428-.096.912-.096z"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

11
registry/bpmct/README.md Normal file
View File

@ -0,0 +1,11 @@
---
display_name: Ben Potter
bio: Tinkerer and Product Manager at Coder
github: bpmct
avatar: ./.images/avatar.png
status: community
---
# Ben Potter
Tinkerer and Product Manager at Coder. Building modules to make dev environments better.

View File

@ -0,0 +1,95 @@
---
display_name: "1Password"
description: "Install the 1Password CLI and VS Code extension in your Coder workspace"
icon: ../../../../.icons/1password.svg
verified: false
tags: [integration, 1password, secrets]
---
# 1Password
Install the [1Password CLI](https://developer.1password.com/docs/cli/)
(`op`) in your Coder workspace and optionally authenticate with a service
account token. Can also install the
[1Password VS Code extension](https://marketplace.visualstudio.com/items?itemName=1Password.op-vscode)
for code-server and VS Code.
```tf
module "onepassword" {
source = "registry.coder.com/bpmct/onepassword/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
service_account_token = var.op_service_account_token
}
```
## Authentication
### Service Account (recommended)
Create a [1Password service account](https://developer.1password.com/docs/service-accounts/get-started/)
and pass the token as a Terraform variable. The module sets
`OP_SERVICE_ACCOUNT_TOKEN` in the workspace so `op` commands work
immediately.
```tf
variable "op_service_account_token" {
type = string
sensitive = true
}
module "onepassword" {
source = "registry.coder.com/bpmct/onepassword/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
service_account_token = var.op_service_account_token
}
```
### Personal Account
Pass your account details and the module will pre-register the account.
You'll be prompted for your password when you run `op signin` in the
terminal.
```tf
module "onepassword" {
source = "registry.coder.com/bpmct/onepassword/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
account_address = "myteam.1password.com"
account_email = "you@example.com"
account_secret_key = var.op_secret_key
}
```
## VS Code Extension
Set `install_vscode_extension = true` to install the 1Password extension
for code-server and VS Code.
```tf
module "onepassword" {
source = "registry.coder.com/bpmct/onepassword/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
service_account_token = var.op_service_account_token
install_vscode_extension = true
}
```
## Custom Scripts
Run custom logic before or after the CLI is installed.
```tf
module "onepassword" {
source = "registry.coder.com/bpmct/onepassword/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
service_account_token = var.op_service_account_token
post_install_script = <<-EOT
op read "op://Vault/item/field" > ~/.secret
EOT
}
```

View File

@ -0,0 +1,112 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 0.17"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "service_account_token" {
type = string
description = "A 1Password service account token. If set, account-based sign-in is skipped."
default = ""
sensitive = true
}
variable "account_address" {
type = string
description = "The 1Password account sign-in address (e.g. myteam.1password.com)."
default = ""
}
variable "account_email" {
type = string
description = "The email address for the 1Password account."
default = ""
}
variable "account_secret_key" {
type = string
description = "The Secret Key for the 1Password account."
default = ""
sensitive = true
}
variable "install_dir" {
type = string
description = "The directory to install the 1Password CLI to."
default = "/usr/local/bin"
}
variable "op_cli_version" {
type = string
description = "The version of the 1Password CLI to install."
default = "latest"
validation {
condition = var.op_cli_version == "latest" || can(regex("^[0-9]+\\.[0-9]+\\.[0-9]+$", var.op_cli_version))
error_message = "op_cli_version must be either 'latest' or a semantic version (e.g., '2.30.0')."
}
}
variable "install_vscode_extension" {
type = bool
description = "Install the 1Password VS Code extension for both VS Code and code-server."
default = false
}
variable "pre_install_script" {
type = string
description = "Custom script to run before installing the 1Password CLI."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing the 1Password CLI."
default = null
}
data "coder_parameter" "account_password" {
count = var.account_address != "" && var.service_account_token == "" ? 1 : 0
type = "string"
name = "op_account_password"
display_name = "1Password Account Password"
description = "Your 1Password account password. Used to sign in to the CLI."
mutable = true
default = ""
}
resource "coder_script" "1password" {
agent_id = var.agent_id
display_name = "1Password CLI"
icon = "/icon/1password.svg"
script = templatefile("${path.module}/run.sh", {
SERVICE_ACCOUNT_TOKEN = var.service_account_token
ACCOUNT_ADDRESS = var.account_address
ACCOUNT_EMAIL = var.account_email
ACCOUNT_SECRET_KEY = var.account_secret_key
ACCOUNT_PASSWORD = var.account_address != "" && var.service_account_token == "" ? data.coder_parameter.account_password[0].value : ""
INSTALL_DIR = var.install_dir
OP_CLI_VERSION = var.op_cli_version
INSTALL_VSCODE_EXTENSION = var.install_vscode_extension
PRE_INSTALL_SCRIPT = var.pre_install_script != null ? base64encode(var.pre_install_script) : ""
POST_INSTALL_SCRIPT = var.post_install_script != null ? base64encode(var.post_install_script) : ""
})
run_on_start = true
start_blocks_login = true
}
resource "coder_env" "op_service_account_token" {
count = var.service_account_token != "" ? 1 : 0
agent_id = var.agent_id
name = "OP_SERVICE_ACCOUNT_TOKEN"
value = var.service_account_token
}

View File

@ -0,0 +1,176 @@
#!/usr/bin/env bash
SERVICE_ACCOUNT_TOKEN="${SERVICE_ACCOUNT_TOKEN}"
ACCOUNT_ADDRESS="${ACCOUNT_ADDRESS}"
ACCOUNT_EMAIL="${ACCOUNT_EMAIL}"
ACCOUNT_SECRET_KEY="${ACCOUNT_SECRET_KEY}"
ACCOUNT_PASSWORD="${ACCOUNT_PASSWORD}"
INSTALL_DIR="${INSTALL_DIR}"
OP_CLI_VERSION="${OP_CLI_VERSION}"
INSTALL_VSCODE_EXTENSION="${INSTALL_VSCODE_EXTENSION}"
PRE_INSTALL_SCRIPT="${PRE_INSTALL_SCRIPT}"
POST_INSTALL_SCRIPT="${POST_INSTALL_SCRIPT}"
fetch() {
if command -v curl > /dev/null 2>&1; then
curl -sSL --fail "$1"
elif command -v wget > /dev/null 2>&1; then
wget -qO- "$1"
else
printf "curl or wget is not installed.\n" && return 1
fi
}
fetch_to_file() {
if command -v curl > /dev/null 2>&1; then
curl -sSL --fail "$2" -o "$1"
elif command -v wget > /dev/null 2>&1; then
wget -O "$1" "$2"
else
printf "curl or wget is not installed.\n" && return 1
fi
}
run_script() {
local ENCODED="$1" LABEL="$2"
if [ -n "$${ENCODED}" ]; then
printf "Running %s script...\n" "$${LABEL}"
SCRIPT_PATH=$(mktemp /tmp/op-"$${LABEL}"-XXXXXX.sh)
printf '%s' "$${ENCODED}" | base64 -d > "$${SCRIPT_PATH}"
chmod +x "$${SCRIPT_PATH}"
# shellcheck disable=SC2288
"$${SCRIPT_PATH}" || printf "WARNING: %s script failed.\n" "$${LABEL}"
rm -f "$${SCRIPT_PATH}"
fi
}
install() {
ARCH=$(uname -m)
if [ "$${ARCH}" = "x86_64" ]; then
ARCH="amd64"
elif [ "$${ARCH}" = "aarch64" ]; then
ARCH="arm64"
else
printf "Unsupported architecture: %s\n" "$${ARCH}" && return 1
fi
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
if [ "$${OS}" != "linux" ] && [ "$${OS}" != "darwin" ]; then
printf "Unsupported OS: %s\n" "$${OS}" && return 1
fi
if [ "$${OP_CLI_VERSION}" = "latest" ]; then
OP_CLI_VERSION=$(fetch "https://app-updates.agilebits.com/check/1/0/CLI2/en/2.0.0/N" \
| grep -oE '"version":"[^"]+"' | head -1 | cut -d'"' -f4) || true
if [ -z "$${OP_CLI_VERSION}" ]; then
printf "Failed to resolve latest version, falling back to 2.30.3.\n"
OP_CLI_VERSION="2.30.3"
fi
fi
printf "1Password CLI version: %s\n" "$${OP_CLI_VERSION}"
if command -v op > /dev/null 2>&1; then
CURRENT_VERSION=$(op --version 2> /dev/null || true)
if [ "$${CURRENT_VERSION}" = "$${OP_CLI_VERSION}" ]; then
printf "Already installed.\n"
return 0
fi
fi
DOWNLOAD_URL="https://cache.agilebits.com/dist/1P/op2/pkg/v$${OP_CLI_VERSION}/op_$${OS}_$${ARCH}_v$${OP_CLI_VERSION}.zip"
TEMP_DIR=$(mktemp -d)
cd "$${TEMP_DIR}" || return 1
if ! fetch_to_file op.zip "$${DOWNLOAD_URL}"; then
rm -rf "$${TEMP_DIR}" && return 1
fi
if command -v unzip > /dev/null 2>&1; then
unzip -o op.zip -d . > /dev/null
elif command -v busybox > /dev/null 2>&1; then
busybox unzip op.zip -d .
else
printf "unzip is not installed.\n"
rm -rf "$${TEMP_DIR}" && return 1
fi
chmod +x op
if [ -n "$${INSTALL_DIR}" ] && [ -w "$${INSTALL_DIR}" ]; then
mv op "$${INSTALL_DIR}/op"
elif [ -n "$${INSTALL_DIR}" ] && sudo mv op "$${INSTALL_DIR}/op" 2> /dev/null; then
true
else
mkdir -p ~/.local/bin && mv op ~/.local/bin/op
INSTALL_DIR=~/.local/bin
fi
printf "Installed to %s.\n" "$${INSTALL_DIR}"
rm -rf "$${TEMP_DIR}"
}
run_script "$${PRE_INSTALL_SCRIPT}" "pre-install"
if ! install; then
printf "Failed to install 1Password CLI.\n"
exit 1
fi
if [ -n "$${SERVICE_ACCOUNT_TOKEN}" ]; then
printf "Service account token configured.\n"
elif [ -n "$${ACCOUNT_ADDRESS}" ] && [ -n "$${ACCOUNT_EMAIL}" ]; then
ADD_ARGS="--address $${ACCOUNT_ADDRESS} --email $${ACCOUNT_EMAIL}"
if [ -n "$${ACCOUNT_SECRET_KEY}" ]; then
ADD_ARGS="$${ADD_ARGS} --secret-key $${ACCOUNT_SECRET_KEY}"
fi
if [ -n "$${ACCOUNT_PASSWORD}" ] && command -v expect > /dev/null 2>&1; then
OP_SESSION=$(expect -c "
log_user 0
spawn op account add $${ADD_ARGS} --raw
expect \"Enter the password*\"
send \"$${ACCOUNT_PASSWORD}\r\"
expect eof
catch wait result
set output \$expect_out(buffer)
puts -nonewline \$output
" 2>&1)
if op account list 2> /dev/null | grep -q "$${ACCOUNT_ADDRESS}"; then
printf "Signed in to %s.\n" "$${ACCOUNT_ADDRESS}"
if [ -n "$${OP_SESSION}" ]; then
mkdir -p "$${HOME}/.op"
SESSION_VAR="OP_SESSION_$(printf '%s' "$${ACCOUNT_ADDRESS}" | tr '.' '_' | tr '-' '_')"
printf 'export %s="%s"\n' "$${SESSION_VAR}" "$${OP_SESSION}" > "$${HOME}/.op/session"
chmod 600 "$${HOME}/.op/session"
for rc in "$${HOME}/.bashrc" "$${HOME}/.zshrc"; do
if [ -f "$${rc}" ] && ! grep -q ".op/session" "$${rc}" 2> /dev/null; then
printf '\n[ -f ~/.op/session ] && . ~/.op/session\n' >> "$${rc}"
fi
done
fi
else
printf "Sign-in failed. Run manually: op signin --account %s\n" "$${ACCOUNT_ADDRESS}"
fi
else
printf "To sign in, run in your terminal:\n"
printf " op account add %s\n" "$${ADD_ARGS}"
fi
fi
if [ "$${INSTALL_VSCODE_EXTENSION}" = "true" ]; then
EXTENSION_ID="1Password.op-vscode"
for _ in 1 2 3 4 5 6; do
command -v code-server > /dev/null 2>&1 || command -v code > /dev/null 2>&1 && break
sleep 5
done
if command -v code-server > /dev/null 2>&1; then
cd /tmp && code-server --install-extension "$${EXTENSION_ID}" --force 2>&1 || true
fi
if command -v code > /dev/null 2>&1; then
cd /tmp && code --install-extension "$${EXTENSION_ID}" --force 2>&1 || true
fi
fi
run_script "$${POST_INSTALL_SCRIPT}" "post-install"