-
Notifications
You must be signed in to change notification settings - Fork 159
feat(python): add Python module #947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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 | ||
| } | ||
| ``` |
| 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", | ||
| }); | ||
| }); |
| 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"] | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+57
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are not using registry/registry/coder-labs/modules/codex/main.tf Lines 159 to 175 in 1a077c8
|
||||||||||||||||||||||||||||||||||||
| 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." | ||
| } | ||
| } |
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using coder-utils instead, it will also write the script and logs to standardized paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check codex module for example:
registry/registry/coder-labs/modules/codex/main.tf
Lines 159 to 175 in 1a077c8