diff --git a/.icons/devcontainers.svg b/.icons/devcontainers.svg new file mode 100644 index 00000000..fb0443bd --- /dev/null +++ b/.icons/devcontainers.svg @@ -0,0 +1,2 @@ + +file_type_devcontainer \ No newline at end of file diff --git a/devcontainers-cli/README.md b/devcontainers-cli/README.md new file mode 100644 index 00000000..5ed3aed8 --- /dev/null +++ b/devcontainers-cli/README.md @@ -0,0 +1,22 @@ +--- +display_name: devcontainers-cli +description: devcontainers-cli module provides an easy way to install @devcontainers/cli into a workspace +icon: ../.icons/devcontainers.svg +verified: true +maintainer_github: coder +tags: [devcontainers] +--- + +# devcontainers-cli + +The devcontainers-cli module provides an easy way to install [`@devcontainers/cli`](https://github.com/devcontainers/cli) into a workspace. It can be used within any workspace as it runs only if +@devcontainers/cli is not installed yet. +`npm` is required and should be pre-installed in order for the module to work. + +```tf +module "devcontainers-cli" { + source = "registry.coder.com/modules/devcontainers-cli/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` diff --git a/devcontainers-cli/main.test.ts b/devcontainers-cli/main.test.ts new file mode 100644 index 00000000..8872f181 --- /dev/null +++ b/devcontainers-cli/main.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, it } from "bun:test"; +import { + execContainer, + executeScriptInContainer, + findResourceInstance, + runContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, + type TerraformState, +} from "../test"; + +const executeScriptInContainerWithPackageManager = async ( + state: TerraformState, + image: string, + packageManager: string, + shell = "sh", +): Promise<{ + exitCode: number; + stdout: string[]; + stderr: string[]; +}> => { + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer(image); + + // Install the specified package manager + if (packageManager === "npm") { + await execContainer(id, [shell, "-c", "apk add nodejs npm"]); + } else if (packageManager === "pnpm") { + await execContainer(id, [ + shell, + "-c", + "apk add nodejs npm && npm install -g pnpm", + ]); + } else if (packageManager === "yarn") { + await execContainer(id, [ + shell, + "-c", + "apk add nodejs npm && npm install -g yarn", + ]); + } + + const resp = await execContainer(id, [shell, "-c", instance.script]); + const stdout = resp.stdout.trim().split("\n"); + const stderr = resp.stderr.trim().split("\n"); + return { + exitCode: resp.exitCode, + stdout, + stderr, + }; +}; + +describe("devcontainers-cli", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "some-agent-id", + }); + + it("misses all package managers", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + }); + const output = await executeScriptInContainer(state, "docker:dind"); + expect(output.exitCode).toBe(1); + expect(output.stderr).toEqual([ + "ERROR: No supported package manager (npm, pnpm, yarn) is installed. Please install one first.", + ]); + }, 15000); + + it("installs devcontainers-cli with npm", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + }); + + const output = await executeScriptInContainerWithPackageManager( + state, + "docker:dind", + "npm", + ); + expect(output.exitCode).toBe(0); + + expect(output.stdout[0]).toEqual( + "Installing @devcontainers/cli using npm...", + ); + expect(output.stdout[output.stdout.length - 1]).toEqual( + "🥳 @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", + ); + }); + + it("installs devcontainers-cli with yarn", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + }); + + const output = await executeScriptInContainerWithPackageManager( + state, + "docker:dind", + "yarn", + ); + expect(output.exitCode).toBe(0); + + expect(output.stdout[0]).toEqual( + "Installing @devcontainers/cli using yarn...", + ); + expect(output.stdout[output.stdout.length - 1]).toEqual( + "🥳 @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", + ); + }); + + it("displays warning if docker is not installed", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "some-agent-id", + }); + + const output = await executeScriptInContainerWithPackageManager( + state, + "alpine", + "npm", + ); + expect(output.exitCode).toBe(0); + + expect(output.stdout[0]).toEqual( + "WARNING: Docker was not found but is required to use @devcontainers/cli, please make sure it is available.", + ); + expect(output.stdout[output.stdout.length - 1]).toEqual( + "🥳 @devcontainers/cli has been installed into /usr/local/bin/devcontainer!", + ); + }); +}); diff --git a/devcontainers-cli/main.tf b/devcontainers-cli/main.tf new file mode 100644 index 00000000..a2aee348 --- /dev/null +++ b/devcontainers-cli/main.tf @@ -0,0 +1,23 @@ +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." +} + +resource "coder_script" "devcontainers-cli" { + agent_id = var.agent_id + display_name = "devcontainers-cli" + icon = "/icon/devcontainers.svg" + script = templatefile("${path.module}/run.sh", {}) + run_on_start = true +} diff --git a/devcontainers-cli/run.sh b/devcontainers-cli/run.sh new file mode 100755 index 00000000..dce7a20e --- /dev/null +++ b/devcontainers-cli/run.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env sh + +# If @devcontainers/cli is already installed, we can skip +if command -v devcontainer > /dev/null 2>&1; then + echo "🥳 @devcontainers/cli is already installed into $(which devcontainer)!" + exit 0 +fi + +# Check if docker is installed +if ! command -v docker > /dev/null 2>&1; then + echo "WARNING: Docker was not found but is required to use @devcontainers/cli, please make sure it is available." +fi + +# Determine the package manager to use: npm, pnpm, or yarn +if command -v pnpm > /dev/null 2>&1; then + PACKAGE_MANAGER="pnpm" +elif command -v yarn > /dev/null 2>&1; then + PACKAGE_MANAGER="yarn" +elif command -v npm > /dev/null 2>&1; then + PACKAGE_MANAGER="npm" +else + echo "ERROR: No supported package manager (npm, pnpm, yarn) is installed. Please install one first." 1>&2 + exit 1 +fi + +echo "Installing @devcontainers/cli using $PACKAGE_MANAGER..." + +# Install @devcontainers/cli using the selected package manager +if [ "$PACKAGE_MANAGER" = "npm" ] || [ "$PACKAGE_MANAGER" = "pnpm" ]; then + $PACKAGE_MANAGER install -g @devcontainers/cli \ + && echo "🥳 @devcontainers/cli has been installed into $(which devcontainer)!" +elif [ "$PACKAGE_MANAGER" = "yarn" ]; then + $PACKAGE_MANAGER global add @devcontainers/cli \ + && echo "🥳 @devcontainers/cli has been installed into $(which devcontainer)!" +fi + +exit 0