Skip to content

feat(devcontainers-cli): add devcontainers-cli module #425

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

Merged
merged 16 commits into from
Apr 16, 2025
2 changes: 2 additions & 0 deletions .icons/devcontainers.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions devcontainers-cli/README.md
Original file line number Diff line number Diff line change
@@ -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
}
```
130 changes: 130 additions & 0 deletions devcontainers-cli/main.test.ts
Original file line number Diff line number Diff line change
@@ -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!",
);
});
});
23 changes: 23 additions & 0 deletions devcontainers-cli/main.tf
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 37 additions & 0 deletions devcontainers-cli/run.sh
Original file line number Diff line number Diff line change
@@ -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