diff --git a/.icons/python.svg b/.icons/python.svg new file mode 100644 index 000000000..e1b14ec64 --- /dev/null +++ b/.icons/python.svg @@ -0,0 +1 @@ +Python \ No newline at end of file diff --git a/registry/attractivetoad/.images/avatar.png b/registry/attractivetoad/.images/avatar.png new file mode 100644 index 000000000..0fd6e6477 Binary files /dev/null and b/registry/attractivetoad/.images/avatar.png differ diff --git a/registry/attractivetoad/README.md b/registry/attractivetoad/README.md new file mode 100644 index 000000000..cde645bd3 --- /dev/null +++ b/registry/attractivetoad/README.md @@ -0,0 +1,15 @@ +--- +display_name: AttractiveToad +bio: Community modules for Coder workspaces. +github: AttractiveToad +avatar: ./.images/avatar.png +status: community +--- + +# AttractiveToad + +Community modules for Coder workspaces. + +## Modules + +- **python**: Install Python 3, pip, venv, and a python alias on Debian/Ubuntu workspaces. diff --git a/registry/attractivetoad/modules/python/README.md b/registry/attractivetoad/modules/python/README.md new file mode 100644 index 000000000..2bfbc0714 --- /dev/null +++ b/registry/attractivetoad/modules/python/README.md @@ -0,0 +1,49 @@ +--- +display_name: Python +description: Install Python 3, pip, venv, and a python alias on Debian/Ubuntu workspaces +icon: ../../../../.icons/python.svg +maintainer_github: AttractiveToad +verified: false +tags: [helper, python] +--- + +# Python + +Installs Python 3 and common Python tooling with `apt-get` on Debian/Ubuntu workspaces. The install script is idempotent: it skips work when all configured packages are already installed. When `python` is missing, the module creates `/usr/local/bin/python` as an alias for `python3`. + +```tf +module "python" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/attractivetoad/python/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Examples + +Install only a subset of Python packages: + +```tf +module "python" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/attractivetoad/python/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + + python_packages = ["python3", "python3-pip"] +} +``` + +Skip the package index update when your image already has a fresh apt cache: + +```tf +module "python" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/attractivetoad/python/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + + update_packages = false +} +``` diff --git a/registry/attractivetoad/modules/python/main.test.ts b/registry/attractivetoad/modules/python/main.test.ts new file mode 100644 index 000000000..65c885a68 --- /dev/null +++ b/registry/attractivetoad/modules/python/main.test.ts @@ -0,0 +1,10 @@ +import { describe } from "bun:test"; +import { runTerraformInit, testRequiredVariables } from "~test"; + +describe("python", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); +}); diff --git a/registry/attractivetoad/modules/python/main.tf b/registry/attractivetoad/modules/python/main.tf new file mode 100644 index 000000000..2f05af330 --- /dev/null +++ b/registry/attractivetoad/modules/python/main.tf @@ -0,0 +1,57 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.13" + } + } +} + +variable "agent_id" { + description = "The ID of a Coder agent." + type = string +} + +variable "python_packages" { + description = "APT packages to install for Python support." + type = list(string) + default = ["python3", "python3-pip", "python3-venv"] +} + +variable "create_python_alias" { + description = "Create a python command that points to python3 when python is missing." + type = bool + default = true +} + +variable "icon" { + description = "Icon to use for the Python install scripts." + type = string + default = "/icon/python.svg" +} + +variable "update_packages" { + description = "Run apt-get update before installing missing packages." + type = bool + default = true +} + +resource "coder_script" "install" { + agent_id = var.agent_id + display_name = "Python: Install Script" + icon = var.icon + run_on_start = true + start_blocks_login = true + script = templatefile("${path.module}/scripts/install.sh.tftpl", { + PYTHON_PACKAGES = join(" ", var.python_packages) + UPDATE_PACKAGES = tostring(var.update_packages) + CREATE_PYTHON_ALIAS = tostring(var.create_python_alias) + }) +} + +output "scripts" { + description = "Ordered list of script names produced by this module, in run order." + value = ["attractivetoad-python-install"] +} diff --git a/registry/attractivetoad/modules/python/main.tftest.hcl b/registry/attractivetoad/modules/python/main.tftest.hcl new file mode 100644 index 000000000..b07c4e5f9 --- /dev/null +++ b/registry/attractivetoad/modules/python/main.tftest.hcl @@ -0,0 +1,39 @@ +mock_provider "coder" {} + +run "plan_with_defaults" { + command = plan + + variables { + agent_id = "example-agent-id" + } + + assert { + condition = join(",", var.python_packages) == "python3,python3-pip,python3-venv" + error_message = "Expected default Python package list." + } + + assert { + condition = var.create_python_alias == true + error_message = "Expected python alias creation to be enabled by default." + } + + assert { + condition = var.update_packages == true + error_message = "Expected package index updates to be enabled by default." + } + + assert { + condition = var.icon == "/icon/python.svg" + error_message = "Expected default icon." + } + + assert { + condition = output.scripts == ["attractivetoad-python-install"] + error_message = "Expected scripts output to expose only the install script by default." + } + + assert { + condition = strcontains(coder_script.install.script, "sudo apt-get -o DPkg::Lock::Timeout=300 update") + error_message = "Expected apt-get update to wait for dpkg locks." + } +} diff --git a/registry/attractivetoad/modules/python/scripts/install.sh.tftpl b/registry/attractivetoad/modules/python/scripts/install.sh.tftpl new file mode 100644 index 000000000..bbd2c0912 --- /dev/null +++ b/registry/attractivetoad/modules/python/scripts/install.sh.tftpl @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +PYTHON_PACKAGES='${PYTHON_PACKAGES}' +UPDATE_PACKAGES='${UPDATE_PACKAGES}' +CREATE_PYTHON_ALIAS='${CREATE_PYTHON_ALIAS}' + +if ! command -v apt-get >/dev/null 2>&1; then + echo "apt-get not found; this module supports Debian/Ubuntu workspaces only." + exit 1 +fi + +if ! command -v dpkg-query >/dev/null 2>&1; then + echo "dpkg-query not found; cannot determine installed packages." + exit 1 +fi + +read -r -a packages <<< "$${PYTHON_PACKAGES}" + +missing_packages=() +for package in "$${packages[@]}"; do + if ! dpkg-query -W -f='$${Status}' "$${package}" 2>/dev/null | grep -q "install ok installed"; then + missing_packages+=("$${package}") + fi +done + +if [ "$${#missing_packages[@]}" -eq 0 ]; then + echo "All requested Python packages are already installed." +else + if [ "$${UPDATE_PACKAGES}" = "true" ]; then + echo "Updating apt package index..." + sudo apt-get -o DPkg::Lock::Timeout=300 update + else + echo "Skipping apt-get update because update_packages=false." + fi + + echo "Installing missing Python packages: $${missing_packages[*]}" + DEBIAN_FRONTEND=noninteractive sudo apt-get -o DPkg::Lock::Timeout=300 install -y "$${missing_packages[@]}" +fi + +if command -v python3 >/dev/null 2>&1; then + python3 --version +fi + +if [ "$${CREATE_PYTHON_ALIAS}" = "true" ] && ! command -v python >/dev/null 2>&1 && command -v python3 >/dev/null 2>&1; then + echo "Creating python alias for python3 at /usr/local/bin/python..." + sudo ln -sf "$(command -v python3)" /usr/local/bin/python +fi + +if command -v python >/dev/null 2>&1; then + python --version +fi + +if command -v pip3 >/dev/null 2>&1; then + pip3 --version || true +elif command -v pip >/dev/null 2>&1; then + pip --version || true +else + echo "pip not found after install; skipping pip version check." +fi