Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .icons/python.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added registry/attractivetoad/.images/avatar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions registry/attractivetoad/README.md
Original file line number Diff line number Diff line change
@@ -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.
49 changes: 49 additions & 0 deletions registry/attractivetoad/modules/python/README.md
Original file line number Diff line number Diff line change
@@ -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
}
```
10 changes: 10 additions & 0 deletions registry/attractivetoad/modules/python/main.test.ts
Original file line number Diff line number Diff line change
@@ -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",
});
});
57 changes: 57 additions & 0 deletions registry/attractivetoad/modules/python/main.tf
Original file line number Diff line number Diff line change
@@ -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"]
}
39 changes: 39 additions & 0 deletions registry/attractivetoad/modules/python/main.tftest.hcl
Original file line number Diff line number Diff line change
@@ -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."
}
}
60 changes: 60 additions & 0 deletions registry/attractivetoad/modules/python/scripts/install.sh.tftpl
Original file line number Diff line number Diff line change
@@ -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