diff --git a/.icons/mise.svg b/.icons/mise.svg new file mode 100644 index 000000000..73ef48894 --- /dev/null +++ b/.icons/mise.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/registry/droopy4096/.images/avatar.png b/registry/droopy4096/.images/avatar.png new file mode 100644 index 000000000..e69de29bb diff --git a/registry/droopy4096/README.md b/registry/droopy4096/README.md new file mode 100644 index 000000000..6ee97a1ec --- /dev/null +++ b/registry/droopy4096/README.md @@ -0,0 +1,11 @@ +--- +display_name: "D Makovey" +bio: "Lifelong tinkerer" +avatar: "./.images/avatar.png" +github: "droopy4096" +status: "community" +--- + +# D Makovey + +Lifelong tinkerer and breaker of things diff --git a/registry/droopy4096/modules/mise-install/README.md b/registry/droopy4096/modules/mise-install/README.md new file mode 100644 index 000000000..3639e4414 --- /dev/null +++ b/registry/droopy4096/modules/mise-install/README.md @@ -0,0 +1,153 @@ +--- +display_name: mise install +description: Install mise and run `mise install` inside a cloned repository. +icon: ../../../../.icons/mise.svg +verified: false +tags: [helper, mise, devtools] +--- + +# mise install + +Installs [`mise`](https://mise.jdx.dev/getting-started.html) on the workspace and runs `mise install` inside a repository directory so language runtimes and tools declared in `.mise.toml` / `.tool-versions` are provisioned before the user starts working. + +Designed to compose with the [`coder/git-clone`](https://registry.coder.com/modules/coder/git-clone) module: pass `module.git_clone.repo_dir` as `repo_dir`. + +```tf +module "mise_install" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/droopy4096/mise-install/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + repo_dir = module.git_clone[count.index].repo_dir +} +``` + +The install script downloads `mise` from `https://mise.run` into `install_dir` (defaults to `$HOME/.local/bin`) and appends `mise activate` to `~/.bashrc` and `~/.zshrc`. The post-install script then runs `mise trust` (optional) and `mise install` inside `repo_dir`. + +## Examples + +### Compose with `coder/git-clone` + +```tf +module "git_clone" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/git-clone/coder" + version = "2.0.3" + agent_id = coder_agent.main.id + url = "https://github.com/example/repo" +} + +module "mise_install" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/droopy4096/mise-install/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + repo_dir = module.git_clone[count.index].repo_dir +} +``` + +### Disable shell activation and `mise trust` + +Use this when the workspace image already activates `mise` globally, or the repository does not use `.mise.toml`: + +```tf +module "mise_install" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/droopy4096/mise-install/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + repo_dir = module.git_clone[count.index].repo_dir + activate_shells = [] + mise_trust = false +} +``` + +### Custom install directory + +```tf +module "mise_install" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/droopy4096/mise-install/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + repo_dir = module.git_clone[count.index].repo_dir + install_dir = "$HOME/.mise/bin" +} +``` + +### Air-gapped / mirrored installer + +Point the installer at an internal mirror, pin the release, and skip the download entirely by using the `mise` already baked into the workspace image: + +```tf +# Fully mirrored: download the installer from an internal HTTPS mirror +# and pin the mise release for reproducibility. +module "mise_install" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/droopy4096/mise-install/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + repo_dir = module.git_clone[count.index].repo_dir + install_url = "https://artifacts.internal.example.com/mise/install.sh" + mise_version = "v2024.9.0" +} + +# Bring-your-own binary: the image already ships mise; skip the download. +module "mise_install_byo" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/droopy4096/mise-install/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + repo_dir = module.git_clone[count.index].repo_dir + install_mise = false + mise_bin = "/opt/mise/bin/mise" # optional; omit if 'mise' is on PATH +} +``` + +## Network egress + +This module reaches the following external endpoints. Mirror or allow-list them (or use `install_mise = false`) in restricted environments: + +| When | Endpoint | Overridable via | +| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Install phase (only when `install_mise = true`) | `https://mise.run` — installer shell script | `install_url` | +| Install phase (transitive from installer) | `https://github.com/jdx/mise/releases/*` — mise binary release assets | Mirror inside `install_url`'s script or use `install_mise = false` + `mise_bin` | +| Post-install phase (`mise install`) | `https://mise.jdx.dev/*` — plugin registry and metadata | `MISE_*` env vars (see mise docs); not exposed by this module | +| Post-install phase (transitive) | Language-runtime hosts (`python.org`, `nodejs.org`, `go.dev`, etc.) — driven by `.mise.toml` / `.tool-versions` in `repo_dir` | Configure `mise` plugins/mirrors in the image | + +If your environment blocks all outbound traffic, set `install_mise = false`, ship `mise` in the image, and mirror the plugin/runtime hosts `mise install` needs at runtime. + +## Execution order + +This module uses [`coder/coder-utils`](https://registry.coder.com/modules/coder/coder-utils) to run two ordered scripts via `coder exp sync`: + +1. **Install Script** — installs `mise` and (optionally) configures shell activation. +2. **Post-Install Script** — waits for the clone to complete, then runs `mise trust` and `mise install` in `repo_dir`. + +Coder starts every `coder_script` on an agent in parallel, so this module's post-install script cannot use `coder exp sync` to wait on `git-clone` (git-clone does not participate in `coder exp sync`). Instead, the post-install script polls for `${repo_dir}/.git` and only runs `mise install` once the clone has finished. The bound is controlled by `wait_seconds` (default `300`). If the timeout expires, the script logs a message and exits `0` without running `mise install`. + +### Custom wait timeout + +```tf +module "mise_install" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/droopy4096/mise-install/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + repo_dir = module.git_clone[count.index].repo_dir + wait_seconds = 900 # wait up to 15 minutes for large clones +} +``` + +## Troubleshooting + +Scripts and logs are written under a per-module root: + +- Scripts: `$HOME/.coder-modules/droopy4096/mise-install/scripts/{install.sh,post_install.sh}` +- Logs: `$HOME/.coder-modules/droopy4096/mise-install/logs/{install.log,post_install.log}` + +If `mise install` fails, inspect `post_install.log` for the exact `mise` output. Common causes: + +- Missing `.mise.toml` / `.tool-versions` in `repo_dir`. +- Untrusted config (rerun with `mise_trust = true`). +- Network egress blocked to endpoints listed under [Network egress](#network-egress). diff --git a/registry/droopy4096/modules/mise-install/main.tf b/registry/droopy4096/modules/mise-install/main.tf new file mode 100644 index 000000000..b700d2297 --- /dev/null +++ b/registry/droopy4096/modules/mise-install/main.tf @@ -0,0 +1,127 @@ +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 "repo_dir" { + description = "Absolute path to the repository where `mise install` should run. Typically wired to `module.git-clone.repo_dir`." + type = string +} + +variable "mise_trust" { + description = "Whether to run `mise trust` on the repository before `mise install`. Required for projects that ship a `.mise.toml`." + type = bool + default = true +} + +variable "activate_shells" { + description = "Shell rc files to append `mise activate` to. Pass an empty list to skip activation." + type = list(string) + default = ["bash", "zsh"] + + validation { + condition = alltrue([for s in var.activate_shells : contains(["bash", "zsh"], s)]) + error_message = "activate_shells entries must be one of: bash, zsh." + } +} + +variable "install_dir" { + description = "Directory to install the `mise` binary into. Passed to the mise installer as `MISE_INSTALL_PATH`'s parent." + type = string + default = "$HOME/.local/bin" +} + +variable "install_mise" { + description = "Whether to download and install `mise`. Set to false in restricted or air-gapped environments where the workspace image already ships `mise`; the module will then only configure shell activation and run `mise install`." + type = bool + default = true +} + +variable "install_url" { + description = "URL of the mise installer script. Override to point at an internal mirror in restricted environments. Ignored when `install_mise = false`." + type = string + default = "https://mise.run" +} + +variable "mise_version" { + description = "Pin the mise release to install by setting `MISE_VERSION` before invoking the installer (e.g. `\"v2024.9.0\"`). Empty means install the latest release advertised by `install_url`. Ignored when `install_mise = false`." + type = string + default = "" +} + +variable "mise_bin" { + description = "Absolute path to a pre-installed `mise` binary (e.g. `/opt/mise/bin/mise` baked into a workspace image). When non-empty, its directory is prepended to PATH so this module and downstream scripts pick it up. Combine with `install_mise = false` to fully opt out of downloading." + type = string + default = "" +} + +variable "wait_seconds" { + description = "Maximum seconds to wait for `repo_dir/.git` to appear (i.e. for the git-clone module to finish) before skipping `mise install`." + type = number + default = 300 + + validation { + condition = var.wait_seconds >= 0 + error_message = "wait_seconds must be non-negative." + } +} + +variable "icon" { + description = "Icon shown in the Coder UI for the coder_script resources this module creates." + type = string + default = "/icon/code.svg" +} + +locals { + module_directory = "$HOME/.coder-modules/droopy4096/mise-install" + + install_script = templatefile("${path.module}/scripts/install.sh.tftpl", { + ARG_INSTALL_DIR = var.install_dir + ARG_ACTIVATE_SHELLS = join(" ", var.activate_shells) + ARG_INSTALL_MISE = tostring(var.install_mise) + ARG_INSTALL_URL = var.install_url + ARG_MISE_VERSION = var.mise_version + ARG_MISE_BIN = var.mise_bin + }) + + post_install_script = templatefile("${path.module}/scripts/post_install.sh.tftpl", { + ARG_REPO_DIR = var.repo_dir + ARG_MISE_TRUST = tostring(var.mise_trust) + ARG_INSTALL_DIR = var.install_dir + ARG_MISE_BIN = var.mise_bin + ARG_WAIT_SECONDS = tostring(var.wait_seconds) + }) +} + +module "coder_utils" { + source = "registry.coder.com/coder/coder-utils/coder" + version = "0.0.1" + + agent_id = var.agent_id + module_directory = local.module_directory + display_name_prefix = "mise" + icon = var.icon + install_script = local.install_script + post_install_script = local.post_install_script +} + +output "repo_dir" { + description = "The repository directory where `mise install` was run." + value = var.repo_dir +} + +output "scripts" { + description = "Ordered list of `coder exp sync` names produced by this module, in run order." + value = module.coder_utils.scripts +} diff --git a/registry/droopy4096/modules/mise-install/mise-install.tftest.hcl b/registry/droopy4096/modules/mise-install/mise-install.tftest.hcl new file mode 100644 index 000000000..6c835bbad --- /dev/null +++ b/registry/droopy4096/modules/mise-install/mise-install.tftest.hcl @@ -0,0 +1,114 @@ +mock_provider "coder" {} + +run "plan_with_required_vars" { + command = plan + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + } +} + +run "invalid_activate_shell_fails" { + command = plan + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + activate_shells = ["fish"] + } + + expect_failures = [var.activate_shells] +} + +run "empty_activate_shells_allowed" { + command = plan + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + activate_shells = [] + } +} + +run "negative_wait_seconds_fails" { + command = plan + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + wait_seconds = -1 + } + + expect_failures = [var.wait_seconds] +} + +run "custom_wait_seconds_allowed" { + command = plan + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + wait_seconds = 900 + } +} + +run "custom_install_url_allowed" { + command = plan + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + install_url = "https://artifacts.internal.example.com/mise/install.sh" + } +} + +run "pinned_mise_version_allowed" { + command = plan + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + mise_version = "v2024.9.0" + } +} + +run "install_mise_false_plans" { + command = plan + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + install_mise = false + } +} + +run "bring_your_own_mise_bin_plans" { + command = plan + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + install_mise = false + mise_bin = "/opt/mise/bin/mise" + } +} + +run "apply_emits_scripts_output" { + command = apply + + variables { + agent_id = "example-agent-id" + repo_dir = "/home/coder/repo" + } + + assert { + condition = length(output.scripts) == 2 + error_message = "Expected scripts output to contain install + post_install entries." + } + + assert { + condition = output.repo_dir == "/home/coder/repo" + error_message = "Expected repo_dir output to echo the input." + } +} diff --git a/registry/droopy4096/modules/mise-install/scripts/install.sh.tftpl b/registry/droopy4096/modules/mise-install/scripts/install.sh.tftpl new file mode 100644 index 000000000..31aa7f47b --- /dev/null +++ b/registry/droopy4096/modules/mise-install/scripts/install.sh.tftpl @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARG_INSTALL_DIR='${ARG_INSTALL_DIR}' +ARG_ACTIVATE_SHELLS='${ARG_ACTIVATE_SHELLS}' +ARG_INSTALL_MISE='${ARG_INSTALL_MISE}' +ARG_INSTALL_URL='${ARG_INSTALL_URL}' +ARG_MISE_VERSION='${ARG_MISE_VERSION}' +ARG_MISE_BIN='${ARG_MISE_BIN}' + +# Expand leading ~ and $HOME in caller-supplied paths (safer than `eval`). +ARG_INSTALL_DIR="$${ARG_INSTALL_DIR/#\~/$${HOME}}" +ARG_INSTALL_DIR="$${ARG_INSTALL_DIR/#\$HOME/$${HOME}}" +if [ -n "$${ARG_MISE_BIN}" ]; then + ARG_MISE_BIN="$${ARG_MISE_BIN/#\~/$${HOME}}" + ARG_MISE_BIN="$${ARG_MISE_BIN/#\$HOME/$${HOME}}" +fi + +# If a pre-installed mise binary was pointed at, prepend its directory +# to PATH so both this script and downstream ones pick it up. +if [ -n "$${ARG_MISE_BIN}" ]; then + if [ ! -x "$${ARG_MISE_BIN}" ]; then + echo "ERROR: mise_bin='$${ARG_MISE_BIN}' is not an executable file." + exit 1 + fi + export PATH="$(dirname "$${ARG_MISE_BIN}"):$${PATH}" +fi + +if [ "$${ARG_INSTALL_MISE}" = "true" ]; then + mkdir -p "$${ARG_INSTALL_DIR}" + if command -v mise >/dev/null 2>&1 || [ -x "$${ARG_INSTALL_DIR}/mise" ]; then + echo "mise already present, skipping download." + else + echo "Installing mise from $${ARG_INSTALL_URL} into $${ARG_INSTALL_DIR}..." + if [ -n "$${ARG_MISE_VERSION}" ]; then + MISE_VERSION="$${ARG_MISE_VERSION}" \ + MISE_INSTALL_PATH="$${ARG_INSTALL_DIR}/mise" \ + curl -fsSL "$${ARG_INSTALL_URL}" | sh + else + MISE_INSTALL_PATH="$${ARG_INSTALL_DIR}/mise" \ + curl -fsSL "$${ARG_INSTALL_URL}" | sh + fi + fi + export PATH="$${ARG_INSTALL_DIR}:$${PATH}" +else + echo "install_mise=false; skipping download. Expecting 'mise' from the workspace image or 'mise_bin'." +fi + +if ! command -v mise >/dev/null 2>&1; then + echo "ERROR: 'mise' is not on PATH after the install phase." + echo "Either set install_mise=true, ensure the workspace image ships mise, or set mise_bin." + exit 1 +fi + +resolved_mise="$(command -v mise)" +echo "Using mise at $${resolved_mise}" +mise --version + +if [ -n "$${ARG_ACTIVATE_SHELLS}" ]; then + for shell in $${ARG_ACTIVATE_SHELLS}; do + rc="$${HOME}/.$${shell}rc" + touch "$${rc}" + if ! grep -qF "mise activate $${shell}" "$${rc}"; then + echo "Appending mise activation to $${rc}" + printf '\n# Added by droopy4096/mise-install\neval "$(%s activate %s)"\n' "$${resolved_mise}" "$${shell}" >> "$${rc}" + else + echo "mise activation already present in $${rc}" + fi + done +fi diff --git a/registry/droopy4096/modules/mise-install/scripts/post_install.sh.tftpl b/registry/droopy4096/modules/mise-install/scripts/post_install.sh.tftpl new file mode 100644 index 000000000..5857d1f10 --- /dev/null +++ b/registry/droopy4096/modules/mise-install/scripts/post_install.sh.tftpl @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARG_REPO_DIR='${ARG_REPO_DIR}' +ARG_MISE_TRUST='${ARG_MISE_TRUST}' +ARG_INSTALL_DIR='${ARG_INSTALL_DIR}' +ARG_MISE_BIN='${ARG_MISE_BIN}' +ARG_WAIT_SECONDS='${ARG_WAIT_SECONDS}' + +# Expand leading ~ and $HOME in caller-supplied paths (safer than `eval`). +ARG_REPO_DIR="$${ARG_REPO_DIR/#\~/$${HOME}}" +ARG_REPO_DIR="$${ARG_REPO_DIR/#\$HOME/$${HOME}}" +ARG_INSTALL_DIR="$${ARG_INSTALL_DIR/#\~/$${HOME}}" +ARG_INSTALL_DIR="$${ARG_INSTALL_DIR/#\$HOME/$${HOME}}" +if [ -n "$${ARG_MISE_BIN}" ]; then + ARG_MISE_BIN="$${ARG_MISE_BIN/#\~/$${HOME}}" + ARG_MISE_BIN="$${ARG_MISE_BIN/#\$HOME/$${HOME}}" + export PATH="$(dirname "$${ARG_MISE_BIN}"):$${PATH}" +fi + +export PATH="$${ARG_INSTALL_DIR}:$${PATH}" + +if ! command -v mise >/dev/null 2>&1; then + echo "mise is not on PATH; the install script must run first (or provide mise via mise_bin / image)." + exit 1 +fi + +# Wait for the clone to finish. Checking for ".git" (not just the directory) +# guards against racing a bare mkdir -p performed before cloning begins. +deadline=$(( $(date +%s) + ARG_WAIT_SECONDS )) +while [ ! -d "$${ARG_REPO_DIR}/.git" ]; do + if [ "$(date +%s)" -ge "$${deadline}" ]; then + echo "Timed out after $${ARG_WAIT_SECONDS}s waiting for $${ARG_REPO_DIR}/.git; skipping 'mise install'." + exit 0 + fi + echo "Waiting for $${ARG_REPO_DIR}/.git to appear..." + sleep 2 +done + +cd "$${ARG_REPO_DIR}" + +if [ "$${ARG_MISE_TRUST}" = "true" ]; then + echo "Running 'mise trust' in $${ARG_REPO_DIR}..." + mise trust || echo "Warning: 'mise trust' failed; continuing." +fi + +echo "Running 'mise install' in $${ARG_REPO_DIR}..." +mise install