diff --git a/.icons/1password.svg b/.icons/1password.svg new file mode 100644 index 00000000..c81c316f --- /dev/null +++ b/.icons/1password.svg @@ -0,0 +1 @@ +1Password \ No newline at end of file diff --git a/registry/bpmct/.images/avatar.png b/registry/bpmct/.images/avatar.png new file mode 100644 index 00000000..57fb2ef8 Binary files /dev/null and b/registry/bpmct/.images/avatar.png differ diff --git a/registry/bpmct/README.md b/registry/bpmct/README.md new file mode 100644 index 00000000..a19a70c0 --- /dev/null +++ b/registry/bpmct/README.md @@ -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. diff --git a/registry/bpmct/modules/onepassword/README.md b/registry/bpmct/modules/onepassword/README.md new file mode 100644 index 00000000..0a911ace --- /dev/null +++ b/registry/bpmct/modules/onepassword/README.md @@ -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 +} +``` diff --git a/registry/bpmct/modules/onepassword/main.tf b/registry/bpmct/modules/onepassword/main.tf new file mode 100644 index 00000000..9f313c43 --- /dev/null +++ b/registry/bpmct/modules/onepassword/main.tf @@ -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 +} diff --git a/registry/bpmct/modules/onepassword/run.sh b/registry/bpmct/modules/onepassword/run.sh new file mode 100644 index 00000000..bbad7493 --- /dev/null +++ b/registry/bpmct/modules/onepassword/run.sh @@ -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"