refactor(vscode-web): migrate to VS Code CLI with code serve-web
- Replace code-server with official VS Code CLI - Download CLI from code.visualstudio.com using cli-alpine-* URLs - Add release_channel variable (stable/insiders) - Add commit_id variable to pin specific VS Code versions - Support offline mode with fallback to code-server or cached vscode-server - Add comprehensive bun tests for settings, extensions, and CLI arguments - Add Terraform tests for variable validation
This commit is contained in:
parent
678c3e631e
commit
b52c0f9f63
@ -8,13 +8,13 @@ tags: [ide, vscode, web]
|
||||
|
||||
# VS Code Web
|
||||
|
||||
Automatically install [Visual Studio Code Server](https://code.visualstudio.com/docs/remote/vscode-server) in a workspace and create an app to access it via the dashboard.
|
||||
Automatically install the [VS Code CLI](https://code.visualstudio.com/docs/editor/command-line) and run `code serve-web` in a workspace to access VS Code via the browser.
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
accept_license = true
|
||||
}
|
||||
@ -30,7 +30,7 @@ module "vscode-web" {
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
install_prefix = "/home/coder/.vscode-web"
|
||||
folder = "/home/coder"
|
||||
@ -44,7 +44,7 @@ module "vscode-web" {
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
|
||||
accept_license = true
|
||||
@ -59,7 +59,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
extensions = ["dracula-theme.theme-dracula"]
|
||||
settings = {
|
||||
@ -69,21 +69,6 @@ module "vscode-web" {
|
||||
}
|
||||
```
|
||||
|
||||
### Pin a specific VS Code Web version
|
||||
|
||||
By default, this module installs the latest. To pin a specific version, retrieve the commit ID from the [VS Code Update API](https://update.code.visualstudio.com/api/commits/stable/server-linux-x64-web) and verify its corresponding release on the [VS Code GitHub Releases](https://github.com/microsoft/vscode/releases).
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
agent_id = coder_agent.example.id
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
accept_license = true
|
||||
}
|
||||
```
|
||||
|
||||
### Open an existing workspace on startup
|
||||
|
||||
To open an existing workspace on startup the `workspace` parameter can be used to represent a path on disk to a `code-workspace` file.
|
||||
@ -93,8 +78,41 @@ Note: Either `workspace` or `folder` can be used, but not both simultaneously. T
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "1.4.3"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
workspace = "/home/coder/coder.code-workspace"
|
||||
accept_license = true
|
||||
}
|
||||
```
|
||||
|
||||
### Use VS Code Insiders
|
||||
|
||||
Use the VS Code Insiders release channel to get the latest features and bug fixes:
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
release_channel = "insiders"
|
||||
accept_license = true
|
||||
}
|
||||
```
|
||||
|
||||
### Pin a specific VS Code version
|
||||
|
||||
Use the `commit_id` variable to pin a specific VS Code Server version by its commit SHA:
|
||||
|
||||
```tf
|
||||
module "vscode-web" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-web/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
accept_license = true
|
||||
}
|
||||
```
|
||||
|
||||
You can find the commit SHA for a specific VS Code version on the [VS Code releases page](https://code.visualstudio.com/updates) or by checking the "About" dialog in VS Code.
|
||||
|
||||
@ -1,42 +1,784 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { runTerraformApply, runTerraformInit } from "~test";
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
beforeAll,
|
||||
afterEach,
|
||||
setDefaultTimeout,
|
||||
} from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
runContainer,
|
||||
execContainer,
|
||||
removeContainer,
|
||||
findResourceInstance,
|
||||
} from "~test";
|
||||
|
||||
// Set timeout to 5 minutes for tests that download VS Code CLI
|
||||
setDefaultTimeout(5 * 60 * 1000);
|
||||
|
||||
let cleanupContainers: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
for (const id of cleanupContainers) {
|
||||
try {
|
||||
await removeContainer(id);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
cleanupContainers = [];
|
||||
});
|
||||
|
||||
describe("vscode-web", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
it("accept_license should be set to true", () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "false",
|
||||
});
|
||||
};
|
||||
expect(t).toThrow("Invalid value for variable");
|
||||
beforeAll(async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
});
|
||||
|
||||
it("use_cached and offline can not be used together", () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "true",
|
||||
use_cached: "true",
|
||||
offline: "true",
|
||||
});
|
||||
};
|
||||
expect(t).toThrow("Offline and Use Cached can not be used together");
|
||||
describe("terraform validation", () => {
|
||||
it("accept_license should be set to true", async () => {
|
||||
try {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: false,
|
||||
});
|
||||
throw new Error("Expected terraform apply to fail");
|
||||
} catch (ex) {
|
||||
expect((ex as Error).message).toContain("Invalid value for variable");
|
||||
}
|
||||
});
|
||||
|
||||
it("use_cached and offline can not be used together", async () => {
|
||||
try {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
use_cached: true,
|
||||
offline: true,
|
||||
});
|
||||
throw new Error("Expected terraform apply to fail");
|
||||
} catch (ex) {
|
||||
expect((ex as Error).message).toContain(
|
||||
"Offline and Use Cached can not be used together",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("offline and extensions can not be used together", async () => {
|
||||
try {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
offline: true,
|
||||
extensions: '["ms-python.python"]',
|
||||
});
|
||||
throw new Error("Expected terraform apply to fail");
|
||||
} catch (ex) {
|
||||
expect((ex as Error).message).toContain(
|
||||
"Offline mode does not allow extensions to be installed",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("workspace and folder can not be used together", async () => {
|
||||
try {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
folder: "/home/coder",
|
||||
workspace: "/home/coder/test.code-workspace",
|
||||
});
|
||||
throw new Error("Expected terraform apply to fail");
|
||||
} catch (ex) {
|
||||
expect((ex as Error).message).toContain(
|
||||
"Set only one of `workspace` or `folder`",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("offline and extensions can not be used together", () => {
|
||||
const t = async () => {
|
||||
await runTerraformApply(import.meta.dir, {
|
||||
describe("script generation", () => {
|
||||
it("generates script with correct port", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: "true",
|
||||
offline: "true",
|
||||
extensions: '["1", "2"]',
|
||||
accept_license: true,
|
||||
port: 8080,
|
||||
});
|
||||
};
|
||||
expect(t).toThrow("Offline mode does not allow extensions to be installed");
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("--port 8080");
|
||||
});
|
||||
|
||||
it("generates script with extensions directory", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
extensions_dir: "/custom/extensions",
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("--extensions-dir=/custom/extensions");
|
||||
});
|
||||
|
||||
it("generates script with telemetry level", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
telemetry_level: "off",
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("--telemetry-level off");
|
||||
});
|
||||
|
||||
it("generates script with disable trust", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
disable_trust: true,
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("--disable-workspace-trust");
|
||||
});
|
||||
|
||||
it("generates script with serve-web command", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("serve-web");
|
||||
expect(script.script).toContain("--accept-server-license-terms");
|
||||
expect(script.script).toContain("--without-connection-token");
|
||||
});
|
||||
|
||||
it("generates script with stable release channel by default", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("build=stable");
|
||||
});
|
||||
|
||||
it("generates script with insiders release channel", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
release_channel: "insiders",
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain("build=insiders");
|
||||
});
|
||||
|
||||
it("generates script without commit-id value when not specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
// The if condition should have an empty string, so no commit-id value is passed
|
||||
expect(script.script).toContain('if [ -n "" ]; then');
|
||||
// Should not contain any actual commit hash
|
||||
expect(script.script).not.toMatch(/--commit-id [a-f0-9]{40}/);
|
||||
});
|
||||
|
||||
it("generates script with commit-id when specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
commit_id: "e54c774e0add60467559eb0d1e229c6452cf8447",
|
||||
});
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
expect(script.script).toContain(
|
||||
"--commit-id e54c774e0add60467559eb0d1e229c6452cf8447",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// More tests depend on shebang refactors
|
||||
describe("container integration tests", () => {
|
||||
it("uses existing code CLI in PATH", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI that logs when serve-web is called
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
if [ "\$1" = "serve-web" ]; then
|
||||
echo "MOCK_SERVER_STARTED with args: \$@"
|
||||
exit 0
|
||||
fi
|
||||
echo "code mock called: \$@"
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
// Run the script - the mock will capture the serve-web call
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("Found VS Code CLI");
|
||||
});
|
||||
|
||||
it("offline mode fails when CLI not present", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
offline: true,
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.stdout).toContain(
|
||||
"Offline mode enabled but no VS Code CLI, code-server, or cached VS Code Server found",
|
||||
);
|
||||
});
|
||||
|
||||
it("offline mode uses code-server as fallback", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
offline: true,
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Install mock code-server in PATH
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code-server << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
echo "MOCK_CODE_SERVER_STARTED with args: $@"
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code-server`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("offline fallback");
|
||||
expect(result.stdout).toContain("Starting code-server");
|
||||
});
|
||||
|
||||
it("offline mode works with pre-installed CLI", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
offline: true,
|
||||
install_prefix: "/tmp/vscode-web",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Pre-install mock code CLI at expected location
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
if [ "\$1" = "serve-web" ]; then
|
||||
echo "MOCK_OFFLINE_SERVER_STARTED"
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /tmp/vscode-web/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("Using cached VS Code CLI");
|
||||
expect(result.stdout).toContain("Starting VS Code Web");
|
||||
});
|
||||
|
||||
it("use_cached mode works with pre-installed CLI", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
use_cached: true,
|
||||
install_prefix: "/tmp/vscode-web",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Pre-install mock code CLI
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /tmp/vscode-web/bin && cat > /tmp/vscode-web/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
if [ "\$1" = "serve-web" ]; then
|
||||
echo "MOCK_CACHED_SERVER_STARTED"
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /tmp/vscode-web/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("Using cached VS Code CLI");
|
||||
});
|
||||
|
||||
it("creates settings file with correct content", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: '{"editor.fontSize": 14}',
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Check that settings file was created
|
||||
const settingsResult = await execContainer(containerId, [
|
||||
"cat",
|
||||
"/root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
|
||||
expect(settingsResult.exitCode).toBe(0);
|
||||
expect(settingsResult.stdout).toContain("editor.fontSize");
|
||||
expect(settingsResult.stdout).toContain("14");
|
||||
});
|
||||
|
||||
it("creates settings file with multiple settings", async () => {
|
||||
const settings = {
|
||||
"editor.fontSize": 16,
|
||||
"editor.tabSize": 2,
|
||||
"workbench.colorTheme": "Dracula",
|
||||
"editor.formatOnSave": true,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Check that settings file was created with all settings
|
||||
const settingsResult = await execContainer(containerId, [
|
||||
"cat",
|
||||
"/root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
|
||||
expect(settingsResult.exitCode).toBe(0);
|
||||
expect(settingsResult.stdout).toContain("editor.fontSize");
|
||||
expect(settingsResult.stdout).toContain("16");
|
||||
expect(settingsResult.stdout).toContain("editor.tabSize");
|
||||
expect(settingsResult.stdout).toContain("2");
|
||||
expect(settingsResult.stdout).toContain("workbench.colorTheme");
|
||||
expect(settingsResult.stdout).toContain("Dracula");
|
||||
expect(settingsResult.stdout).toContain("editor.formatOnSave");
|
||||
expect(settingsResult.stdout).toContain("true");
|
||||
});
|
||||
|
||||
it("creates settings file in correct directory structure", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: '{"test.setting": "value"}',
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Verify directory structure was created
|
||||
const dirResult = await execContainer(containerId, [
|
||||
"ls",
|
||||
"-la",
|
||||
"/root/.vscode-server/data/Machine/",
|
||||
]);
|
||||
|
||||
expect(dirResult.exitCode).toBe(0);
|
||||
expect(dirResult.stdout).toContain("settings.json");
|
||||
|
||||
// Verify parent directories exist
|
||||
const parentDirResult = await execContainer(containerId, [
|
||||
"ls",
|
||||
"-la",
|
||||
"/root/.vscode-server/data/",
|
||||
]);
|
||||
|
||||
expect(parentDirResult.exitCode).toBe(0);
|
||||
expect(parentDirResult.stdout).toContain("Machine");
|
||||
});
|
||||
|
||||
it("does not overwrite existing settings file", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: '{"new.setting": "new_value"}',
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
// Pre-create an existing settings file
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /root/.vscode-server/data/Machine && echo '{"existing.setting": "existing_value"}' > /root/.vscode-server/data/Machine/settings.json`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Check that existing settings file was NOT overwritten
|
||||
const settingsResult = await execContainer(containerId, [
|
||||
"cat",
|
||||
"/root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
|
||||
expect(settingsResult.exitCode).toBe(0);
|
||||
// Should contain existing setting, not the new one
|
||||
expect(settingsResult.stdout).toContain("existing.setting");
|
||||
expect(settingsResult.stdout).toContain("existing_value");
|
||||
expect(settingsResult.stdout).not.toContain("new.setting");
|
||||
});
|
||||
|
||||
it("creates valid JSON settings file", async () => {
|
||||
const settings = {
|
||||
"editor.fontSize": 14,
|
||||
"editor.wordWrap": "on",
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.autoSaveDelay": 1000,
|
||||
};
|
||||
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
settings: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Install jq and create mock code CLI
|
||||
await execContainer(containerId, ["apt-get", "update", "-qq"]);
|
||||
await execContainer(containerId, [
|
||||
"apt-get",
|
||||
"install",
|
||||
"-y",
|
||||
"-qq",
|
||||
"jq",
|
||||
]);
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Validate JSON using jq
|
||||
const jsonValidResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
"jq '.' /root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
|
||||
expect(jsonValidResult.exitCode).toBe(0);
|
||||
|
||||
// Extract specific values using jq
|
||||
const fontSizeResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
"jq '.\"editor.fontSize\"' /root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
expect(fontSizeResult.stdout.trim()).toBe("14");
|
||||
|
||||
const wordWrapResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
"jq '.\"editor.wordWrap\"' /root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
expect(wordWrapResult.stdout.trim()).toBe('"on"');
|
||||
|
||||
const autoSaveDelayResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
"jq '.\"files.autoSaveDelay\"' /root/.vscode-server/data/Machine/settings.json",
|
||||
]);
|
||||
expect(autoSaveDelayResult.stdout.trim()).toBe("1000");
|
||||
});
|
||||
|
||||
it("installs extensions", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
extensions: '["ms-python.python", "golang.go"]',
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI that logs extension installs
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
if [ "\$1" = "--install-extension" ]; then
|
||||
echo "MOCK_EXTENSION_INSTALL: \$2"
|
||||
fi
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toContain("Installing extension");
|
||||
expect(result.stdout).toContain("ms-python.python");
|
||||
expect(result.stdout).toContain("golang.go");
|
||||
});
|
||||
|
||||
it("runs with correct server arguments", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
port: 9999,
|
||||
telemetry_level: "off",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI that captures all arguments
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
echo "MOCK_CODE_ARGS: \$@"
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
const result = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
// Check the output contains expected port message
|
||||
expect(result.stdout).toContain("Starting VS Code Web on port 9999");
|
||||
});
|
||||
|
||||
it("passes commit-id to code CLI when specified", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
commit_id: "abc123def456",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Create a mock code CLI that logs arguments to the log file (where output is redirected)
|
||||
await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
`mkdir -p /usr/local/bin && cat > /usr/local/bin/code << 'MOCKEOF'
|
||||
#!/bin/bash
|
||||
echo "MOCK_CODE_ARGS: $@"
|
||||
exit 0
|
||||
MOCKEOF
|
||||
chmod +x /usr/local/bin/code`,
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
await execContainer(containerId, ["bash", "-c", script.script]);
|
||||
|
||||
// Wait briefly for background process to write to log
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Check the log file for the arguments (code CLI output goes there)
|
||||
const logResult = await execContainer(containerId, [
|
||||
"cat",
|
||||
"/tmp/vscode-web.log",
|
||||
]);
|
||||
|
||||
expect(logResult.exitCode).toBe(0);
|
||||
expect(logResult.stdout).toContain("--commit-id abc123def456");
|
||||
});
|
||||
|
||||
// This test downloads and starts the real VS Code server
|
||||
it("starts real VS Code CLI and responds to healthcheck (requires network)", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
accept_license: true,
|
||||
port: 13338,
|
||||
install_prefix: "/tmp/vscode-web",
|
||||
});
|
||||
|
||||
const containerId = await runContainer("ubuntu:22.04");
|
||||
cleanupContainers.push(containerId);
|
||||
|
||||
// Install curl for downloading CLI and healthcheck
|
||||
await execContainer(containerId, ["apt-get", "update", "-qq"]);
|
||||
await execContainer(containerId, [
|
||||
"apt-get",
|
||||
"install",
|
||||
"-y",
|
||||
"-qq",
|
||||
"curl",
|
||||
]);
|
||||
|
||||
const script = findResourceInstance(state, "coder_script");
|
||||
|
||||
// Run the script - it will start the server in background
|
||||
const startResult = await execContainer(containerId, [
|
||||
"bash",
|
||||
"-c",
|
||||
script.script,
|
||||
]);
|
||||
|
||||
expect(startResult.exitCode).toBe(0);
|
||||
expect(startResult.stdout).toContain("Starting VS Code Web");
|
||||
|
||||
// Wait for server to start and check healthcheck
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
||||
const healthResult = await execContainer(containerId, [
|
||||
"curl",
|
||||
"-s",
|
||||
"-o",
|
||||
"/dev/null",
|
||||
"-w",
|
||||
"%{http_code}",
|
||||
"http://127.0.0.1:13338/healthz",
|
||||
]);
|
||||
|
||||
// Server should respond (200, 202, or 404 is acceptable - means server is running)
|
||||
expect(["200", "202", "404"]).toContain(healthResult.stdout.trim());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -59,12 +59,6 @@ variable "install_prefix" {
|
||||
default = "/tmp/vscode-web"
|
||||
}
|
||||
|
||||
variable "commit_id" {
|
||||
type = string
|
||||
description = "Specify the commit ID of the VS Code Web binary to pin to a specific version. If left empty, the latest stable version is used."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "extensions" {
|
||||
type = list(string)
|
||||
description = "A list of extensions to install."
|
||||
@ -148,22 +142,28 @@ variable "subdomain" {
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "platform" {
|
||||
type = string
|
||||
description = "The platform to use for the VS Code Web."
|
||||
default = ""
|
||||
validation {
|
||||
condition = var.platform == "" || var.platform == "linux" || var.platform == "darwin" || var.platform == "alpine" || var.platform == "win32"
|
||||
error_message = "Incorrect value. Please set either 'linux', 'darwin', or 'alpine' or 'win32'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "workspace" {
|
||||
type = string
|
||||
description = "Path to a .code-workspace file to open in vscode-web."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "release_channel" {
|
||||
type = string
|
||||
description = "The release channel for VS Code CLI (stable or insiders)."
|
||||
default = "stable"
|
||||
validation {
|
||||
condition = var.release_channel == "stable" || var.release_channel == "insiders"
|
||||
error_message = "Incorrect value. Please set either 'stable' or 'insiders'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "commit_id" {
|
||||
type = string
|
||||
description = "The commit SHA to use for the VS Code Server. Leave empty to use the latest version."
|
||||
default = ""
|
||||
}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
@ -187,8 +187,8 @@ resource "coder_script" "vscode-web" {
|
||||
WORKSPACE : var.workspace,
|
||||
AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions,
|
||||
SERVER_BASE_PATH : local.server_base_path,
|
||||
RELEASE_CHANNEL : var.release_channel,
|
||||
COMMIT_ID : var.commit_id,
|
||||
PLATFORM : var.platform,
|
||||
})
|
||||
run_on_start = true
|
||||
|
||||
|
||||
@ -1,138 +1,325 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BOLD='\033[0;1m'
|
||||
RESET='\033[0m'
|
||||
CODE='\033[36;40;1m'
|
||||
EXTENSIONS=("${EXTENSIONS}")
|
||||
VSCODE_WEB="${INSTALL_PREFIX}/bin/code-server"
|
||||
|
||||
# Set extension directory
|
||||
# Set extension directory argument
|
||||
EXTENSION_ARG=""
|
||||
if [ -n "${EXTENSIONS_DIR}" ]; then
|
||||
EXTENSION_ARG="--extensions-dir=${EXTENSIONS_DIR}"
|
||||
fi
|
||||
|
||||
# Set extension directory
|
||||
# Set server base path argument
|
||||
SERVER_BASE_PATH_ARG=""
|
||||
if [ -n "${SERVER_BASE_PATH}" ]; then
|
||||
SERVER_BASE_PATH_ARG="--server-base-path=${SERVER_BASE_PATH}"
|
||||
fi
|
||||
|
||||
# Set disable workspace trust
|
||||
# Set disable workspace trust argument
|
||||
DISABLE_TRUST_ARG=""
|
||||
if [ "${DISABLE_TRUST}" = true ]; then
|
||||
DISABLE_TRUST_ARG="--disable-workspace-trust"
|
||||
fi
|
||||
|
||||
run_vscode_web() {
|
||||
echo "👷 Running $VSCODE_WEB serve-local $EXTENSION_ARG $SERVER_BASE_PATH_ARG $DISABLE_TRUST_ARG --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL} in the background..."
|
||||
echo "Check logs at ${LOG_PATH}!"
|
||||
"$VSCODE_WEB" serve-local "$EXTENSION_ARG" "$SERVER_BASE_PATH_ARG" "$DISABLE_TRUST_ARG" --port "${PORT}" --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level "${TELEMETRY_LEVEL}" > "${LOG_PATH}" 2>&1 &
|
||||
# Check if code CLI is installed
|
||||
check_code_cli() {
|
||||
if command -v code > /dev/null 2>&1; then
|
||||
echo "code"
|
||||
return 0
|
||||
fi
|
||||
if [ -f "${INSTALL_PREFIX}/bin/code" ]; then
|
||||
echo "${INSTALL_PREFIX}/bin/code"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if the settings file exists...
|
||||
# Check if code-server is installed (fallback option)
|
||||
check_code_server() {
|
||||
if command -v code-server > /dev/null 2>&1; then
|
||||
echo "code-server"
|
||||
return 0
|
||||
fi
|
||||
if [ -f "${INSTALL_PREFIX}/bin/code-server" ]; then
|
||||
echo "${INSTALL_PREFIX}/bin/code-server"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Find existing vscode-server binary (used by code serve-web internally)
|
||||
find_vscode_server() {
|
||||
# Check common locations for pre-downloaded vscode-server
|
||||
local server_dirs=(
|
||||
"$HOME/.vscode-server/bin"
|
||||
"$HOME/.vscode/cli/serve-web"
|
||||
)
|
||||
for dir in "$${server_dirs[@]}"; do
|
||||
if [ -d "$dir" ]; then
|
||||
# Find the most recent server version
|
||||
local latest=$(ls -t "$dir" 2>/dev/null | head -1)
|
||||
if [ -n "$latest" ] && [ -f "$dir/$latest/bin/code-server" ]; then
|
||||
echo "$dir/$latest/bin/code-server"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$latest" ] && [ -f "$dir/$latest/code-server" ]; then
|
||||
echo "$dir/$latest/code-server"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Install VS Code CLI if not present
|
||||
install_code_cli() {
|
||||
printf "$${BOLD}Installing VS Code CLI...$${RESET}\n"
|
||||
|
||||
# Detect architecture
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="x64" ;;
|
||||
aarch64 | arm64) ARCH="arm64" ;;
|
||||
armv7l) ARCH="armhf" ;;
|
||||
*)
|
||||
echo "Unsupported architecture: $ARCH"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect platform
|
||||
# Note: VS Code CLI uses 'alpine' for all Linux distributions
|
||||
PLATFORM=$(uname -s)
|
||||
case "$PLATFORM" in
|
||||
Linux)
|
||||
PLATFORM="alpine"
|
||||
;;
|
||||
Darwin)
|
||||
PLATFORM="darwin"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported platform: $PLATFORM"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Create install directory
|
||||
mkdir -p "${INSTALL_PREFIX}/bin"
|
||||
|
||||
# Download VS Code CLI
|
||||
CLI_URL="https://code.visualstudio.com/sha/download?build=${RELEASE_CHANNEL}&os=cli-$PLATFORM-$ARCH"
|
||||
printf "Downloading VS Code CLI from %s\n" "$CLI_URL"
|
||||
|
||||
if command -v curl > /dev/null 2>&1; then
|
||||
curl -fsSL "$CLI_URL" -o "/tmp/vscode-cli.tar.gz"
|
||||
elif command -v wget > /dev/null 2>&1; then
|
||||
wget -q "$CLI_URL" -O "/tmp/vscode-cli.tar.gz"
|
||||
else
|
||||
echo "Neither curl nor wget is available. Please install one of them."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract CLI
|
||||
tar -xzf /tmp/vscode-cli.tar.gz -C "${INSTALL_PREFIX}/bin"
|
||||
rm -f /tmp/vscode-cli.tar.gz
|
||||
|
||||
# The CLI binary is named 'code'
|
||||
if [ -f "${INSTALL_PREFIX}/bin/code" ]; then
|
||||
chmod +x "${INSTALL_PREFIX}/bin/code"
|
||||
export PATH="${INSTALL_PREFIX}/bin:$PATH"
|
||||
printf "$${BOLD}VS Code CLI installed successfully.$${RESET}\n"
|
||||
else
|
||||
echo "Failed to install VS Code CLI"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run VS Code Web using the code CLI (serve-web command)
|
||||
run_vscode_web_cli() {
|
||||
local CODE_CMD="$1"
|
||||
|
||||
# Build the command arguments
|
||||
ARGS="serve-web --port ${PORT} --host 127.0.0.1 --accept-server-license-terms --without-connection-token --telemetry-level ${TELEMETRY_LEVEL}"
|
||||
|
||||
if [ -n "$EXTENSION_ARG" ]; then
|
||||
ARGS="$ARGS $EXTENSION_ARG"
|
||||
fi
|
||||
|
||||
if [ -n "$SERVER_BASE_PATH_ARG" ]; then
|
||||
ARGS="$ARGS $SERVER_BASE_PATH_ARG"
|
||||
fi
|
||||
|
||||
if [ -n "$DISABLE_TRUST_ARG" ]; then
|
||||
ARGS="$ARGS $DISABLE_TRUST_ARG"
|
||||
fi
|
||||
|
||||
if [ -n "${COMMIT_ID}" ]; then
|
||||
ARGS="$ARGS --commit-id ${COMMIT_ID}"
|
||||
fi
|
||||
|
||||
printf "Starting VS Code Web on port ${PORT}...\n"
|
||||
printf "Check logs at ${LOG_PATH}\n"
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
"$CODE_CMD" $ARGS > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Run VS Code Web using code-server (fallback for offline mode)
|
||||
run_code_server() {
|
||||
local SERVER_CMD="$1"
|
||||
|
||||
printf "Starting code-server on port ${PORT}...\n"
|
||||
printf "Check logs at ${LOG_PATH}\n"
|
||||
|
||||
# Build arguments for code-server
|
||||
ARGS="--port ${PORT} --host 127.0.0.1 --auth none"
|
||||
|
||||
if [ -n "$EXTENSION_ARG" ]; then
|
||||
ARGS="$ARGS $EXTENSION_ARG"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
"$SERVER_CMD" $ARGS > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
# Run VS Code Web using vscode-server binary directly
|
||||
run_vscode_server() {
|
||||
local SERVER_CMD="$1"
|
||||
|
||||
printf "Starting VS Code Server on port ${PORT}...\n"
|
||||
printf "Check logs at ${LOG_PATH}\n"
|
||||
|
||||
# Build arguments for vscode-server
|
||||
ARGS="--port ${PORT} --host 127.0.0.1 --without-connection-token --accept-server-license-terms --telemetry-level ${TELEMETRY_LEVEL}"
|
||||
|
||||
if [ -n "$EXTENSION_ARG" ]; then
|
||||
ARGS="$ARGS $EXTENSION_ARG"
|
||||
fi
|
||||
|
||||
if [ -n "$SERVER_BASE_PATH_ARG" ]; then
|
||||
ARGS="$ARGS $SERVER_BASE_PATH_ARG"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
"$SERVER_CMD" serve-local $ARGS > "${LOG_PATH}" 2>&1 &
|
||||
}
|
||||
|
||||
install_extensions() {
|
||||
local CODE_CMD="$1"
|
||||
|
||||
# Install specified extensions
|
||||
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
||||
for extension in "$${EXTENSIONLIST[@]}"; do
|
||||
if [ -z "$extension" ]; then
|
||||
continue
|
||||
fi
|
||||
printf "Installing extension $${CODE}$extension$${RESET}...\n"
|
||||
output=$("$CODE_CMD" $EXTENSION_ARG --install-extension "$extension" --force 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install extension: $extension: $output"
|
||||
fi
|
||||
done
|
||||
|
||||
# Auto-install extensions from workspace or folder
|
||||
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
|
||||
if ! command -v jq > /dev/null; then
|
||||
echo "jq is required to install extensions from a workspace file."
|
||||
else
|
||||
if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
|
||||
printf "Installing extensions from %s...\n" "${WORKSPACE}"
|
||||
extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]')
|
||||
for extension in $extensions; do
|
||||
"$CODE_CMD" $EXTENSION_ARG --install-extension "$extension" --force
|
||||
done
|
||||
else
|
||||
WORKSPACE_DIR="$HOME"
|
||||
if [ -n "${FOLDER}" ]; then
|
||||
WORKSPACE_DIR="${FOLDER}"
|
||||
fi
|
||||
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
|
||||
printf "Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
|
||||
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]')
|
||||
for extension in $extensions; do
|
||||
"$CODE_CMD" $EXTENSION_ARG --install-extension "$extension" --force
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Create settings file if it doesn't exist
|
||||
if [ ! -f ~/.vscode-server/data/Machine/settings.json ]; then
|
||||
echo "⚙️ Creating settings file..."
|
||||
printf "Creating settings file...\n"
|
||||
mkdir -p ~/.vscode-server/data/Machine
|
||||
echo "${SETTINGS}" > ~/.vscode-server/data/Machine/settings.json
|
||||
fi
|
||||
|
||||
# Check if vscode-server is already installed for offline or cached mode
|
||||
if [ -f "$VSCODE_WEB" ]; then
|
||||
if [ "${OFFLINE}" = true ] || [ "${USE_CACHED}" = true ]; then
|
||||
echo "🥳 Found a copy of VS Code Web"
|
||||
run_vscode_web
|
||||
# Determine which command to use
|
||||
CODE_CMD=""
|
||||
RUN_MODE=""
|
||||
|
||||
# Check for code CLI first (preferred)
|
||||
if CODE_CMD=$(check_code_cli); then
|
||||
printf "$${BOLD}Found VS Code CLI at $CODE_CMD$${RESET}\n"
|
||||
RUN_MODE="cli"
|
||||
fi
|
||||
|
||||
# Handle offline mode
|
||||
if [ "${OFFLINE}" = true ]; then
|
||||
if [ -n "$CODE_CMD" ]; then
|
||||
# Check if vscode-server is already downloaded (code serve-web won't need to download)
|
||||
if VSCODE_SERVER=$(find_vscode_server); then
|
||||
printf "Found cached VS Code Server at $VSCODE_SERVER\n"
|
||||
printf "Using cached VS Code CLI.\n"
|
||||
run_vscode_web_cli "$CODE_CMD"
|
||||
exit 0
|
||||
fi
|
||||
# Code CLI exists but vscode-server not cached - try using it anyway
|
||||
# (it might work if server was pre-downloaded, or fail gracefully)
|
||||
printf "Warning: VS Code Server may not be cached. Attempting to start...\n"
|
||||
printf "Using cached VS Code CLI.\n"
|
||||
run_vscode_web_cli "$CODE_CMD"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
# Offline mode always expects a copy of vscode-server to be present
|
||||
if [ "${OFFLINE}" = true ]; then
|
||||
echo "Failed to find a copy of VS Code Web"
|
||||
|
||||
# Try code-server as fallback for offline mode
|
||||
if SERVER_CMD=$(check_code_server); then
|
||||
printf "$${BOLD}Found code-server at $SERVER_CMD (offline fallback)$${RESET}\n"
|
||||
run_code_server "$SERVER_CMD"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Try vscode-server binary directly
|
||||
if VSCODE_SERVER=$(find_vscode_server); then
|
||||
printf "$${BOLD}Found VS Code Server at $VSCODE_SERVER (offline fallback)$${RESET}\n"
|
||||
run_vscode_server "$VSCODE_SERVER"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Offline mode enabled but no VS Code CLI, code-server, or cached VS Code Server found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create install prefix
|
||||
mkdir -p ${INSTALL_PREFIX}
|
||||
|
||||
printf "$${BOLD}Installing Microsoft Visual Studio Code Server!\n"
|
||||
|
||||
# Download and extract vscode-server
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH="x64" ;;
|
||||
aarch64) ARCH="arm64" ;;
|
||||
*)
|
||||
echo "Unsupported architecture"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Detect the platform
|
||||
if [ -n "${PLATFORM}" ]; then
|
||||
DETECTED_PLATFORM="${PLATFORM}"
|
||||
elif [ -f /etc/alpine-release ] || grep -qi 'ID=alpine' /etc/os-release 2> /dev/null || command -v apk > /dev/null 2>&1; then
|
||||
DETECTED_PLATFORM="alpine"
|
||||
elif [ "$(uname -s)" = "Darwin" ]; then
|
||||
DETECTED_PLATFORM="darwin"
|
||||
else
|
||||
DETECTED_PLATFORM="linux"
|
||||
# Handle use_cached mode
|
||||
if [ "${USE_CACHED}" = true ] && [ -n "$CODE_CMD" ]; then
|
||||
printf "Using cached VS Code CLI.\n"
|
||||
install_extensions "$CODE_CMD"
|
||||
run_vscode_web_cli "$CODE_CMD"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if a specific VS Code Web commit ID was provided
|
||||
if [ -n "${COMMIT_ID}" ]; then
|
||||
HASH="${COMMIT_ID}"
|
||||
else
|
||||
HASH=$(curl -fsSL https://update.code.visualstudio.com/api/commits/stable/server-$DETECTED_PLATFORM-$ARCH-web | cut -d '"' -f 2)
|
||||
fi
|
||||
printf "$${BOLD}VS Code Web commit id version $HASH.\n"
|
||||
|
||||
output=$(curl -fsSL "https://vscode.download.prss.microsoft.com/dbazure/download/stable/$HASH/vscode-server-$DETECTED_PLATFORM-$ARCH-web.tar.gz" | tar -xz -C "${INSTALL_PREFIX}" --strip-components 1)
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install Microsoft Visual Studio Code Server: $output"
|
||||
exit 1
|
||||
fi
|
||||
printf "$${BOLD}VS Code Web has been installed.\n"
|
||||
|
||||
# Install each extension...
|
||||
IFS=',' read -r -a EXTENSIONLIST <<< "$${EXTENSIONS}"
|
||||
# shellcheck disable=SC2066
|
||||
for extension in "$${EXTENSIONLIST[@]}"; do
|
||||
if [ -z "$extension" ]; then
|
||||
continue
|
||||
fi
|
||||
printf "🧩 Installing extension $${CODE}$extension$${RESET}...\n"
|
||||
output=$($VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Failed to install extension: $extension: $output"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${AUTO_INSTALL_EXTENSIONS}" = true ]; then
|
||||
if ! command -v jq > /dev/null; then
|
||||
echo "jq is required to install extensions from a workspace file."
|
||||
else
|
||||
# Prefer WORKSPACE if set and points to a file
|
||||
if [ -n "${WORKSPACE}" ] && [ -f "${WORKSPACE}" ]; then
|
||||
printf "🧩 Installing extensions from %s...\n" "${WORKSPACE}"
|
||||
# Strip single-line comments then parse .extensions.recommendations[]
|
||||
extensions=$(sed 's|//.*||g' "${WORKSPACE}" | jq -r '(.extensions.recommendations // [])[]')
|
||||
for extension in $extensions; do
|
||||
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
|
||||
done
|
||||
else
|
||||
# Fallback to folder-based .vscode/extensions.json (existing behavior)
|
||||
WORKSPACE_DIR="$HOME"
|
||||
if [ -n "${FOLDER}" ]; then
|
||||
WORKSPACE_DIR="${FOLDER}"
|
||||
fi
|
||||
if [ -f "$WORKSPACE_DIR/.vscode/extensions.json" ]; then
|
||||
printf "🧩 Installing extensions from %s/.vscode/extensions.json...\n" "$WORKSPACE_DIR"
|
||||
extensions=$(sed 's|//.*||g' "$WORKSPACE_DIR/.vscode/extensions.json" | jq -r '.recommendations[]')
|
||||
for extension in $extensions; do
|
||||
$VSCODE_WEB "$EXTENSION_ARG" --install-extension "$extension" --force
|
||||
done
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# Install VS Code CLI if not present
|
||||
if [ -z "$CODE_CMD" ]; then
|
||||
install_code_cli
|
||||
CODE_CMD="${INSTALL_PREFIX}/bin/code"
|
||||
RUN_MODE="cli"
|
||||
fi
|
||||
|
||||
run_vscode_web
|
||||
# Install extensions
|
||||
install_extensions "$CODE_CMD"
|
||||
|
||||
# Run VS Code Web
|
||||
run_vscode_web_cli "$CODE_CMD"
|
||||
|
||||
151
registry/coder/modules/vscode-web/vscode-web.tftest.hcl
Normal file
151
registry/coder/modules/vscode-web/vscode-web.tftest.hcl
Normal file
@ -0,0 +1,151 @@
|
||||
run "required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
}
|
||||
}
|
||||
|
||||
run "accept_license_required" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = false
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.accept_license
|
||||
]
|
||||
}
|
||||
|
||||
run "offline_and_use_cached_conflict" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
use_cached = true
|
||||
offline = true
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.vscode-web
|
||||
]
|
||||
}
|
||||
|
||||
run "offline_disallows_extensions" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
offline = true
|
||||
extensions = ["ms-python.python", "golang.go"]
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.vscode-web
|
||||
]
|
||||
}
|
||||
|
||||
run "workspace_and_folder_conflict" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
folder = "/home/coder/project"
|
||||
workspace = "/home/coder/project.code-workspace"
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.vscode-web
|
||||
]
|
||||
}
|
||||
|
||||
run "url_with_folder_query" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
folder = "/home/coder/project"
|
||||
port = 13338
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.vscode-web.url == "http://localhost:13338?folder=%2Fhome%2Fcoder%2Fproject"
|
||||
error_message = "coder_app URL must include encoded folder query param"
|
||||
}
|
||||
}
|
||||
|
||||
run "url_with_workspace_query" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
workspace = "/home/coder/project.code-workspace"
|
||||
port = 13338
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_app.vscode-web.url == "http://localhost:13338?workspace=%2Fhome%2Fcoder%2Fproject.code-workspace"
|
||||
error_message = "coder_app URL must include encoded workspace query param"
|
||||
}
|
||||
}
|
||||
|
||||
run "release_channel_stable" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
release_channel = "stable"
|
||||
}
|
||||
}
|
||||
|
||||
run "release_channel_insiders" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
release_channel = "insiders"
|
||||
}
|
||||
}
|
||||
|
||||
run "release_channel_invalid" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
release_channel = "invalid"
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.release_channel
|
||||
]
|
||||
}
|
||||
|
||||
run "commit_id_empty_by_default" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
}
|
||||
}
|
||||
|
||||
run "commit_id_with_value" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "foo"
|
||||
accept_license = true
|
||||
commit_id = "e54c774e0add60467559eb0d1e229c6452cf8447"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user