Compare commits

..

1 Commits

Author SHA1 Message Date
Atif Ali eafdabc48c fix(zed): fix settings JSON parsing with base64 encoding
The previous implementation used quote escaping which produced invalid JSON:
  SETTINGS_JSON='{"theme":"dark"}'

This broke jq parsing because the escaped quotes are not valid JSON.

Changed to use base64 encoding internally:
  settings_b64 = base64encode(var.settings)
  echo -n "${SETTINGS_B64}" | base64 -d > settings.json

User interface remains the same - pass JSON via jsonencode() or heredoc.

Simplified script to just write settings directly (no jq merging) since
Zed already handles merging remote and local settings internally.

Also added end-to-end container tests and fixed existing tests to use
override_data for workspace mocking.
2025-12-29 16:35:47 +00:00
5 changed files with 149 additions and 80 deletions
+12 -16
View File
@@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
@@ -28,7 +28,7 @@ module "git-clone" {
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
base_dir = "~/projects/coder"
@@ -43,12 +43,11 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
}
data "coder_external_auth" "github" {
id = "github"
}
@@ -70,12 +69,11 @@ data "coder_parameter" "git_repo" {
module "git_clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = data.coder_parameter.git_repo.value
}
# Create a code-server instance for the cloned repository
module "code-server" {
count = data.coder_workspace.me.start_count
@@ -105,14 +103,13 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.example.com/coder/coder/tree/feat/example"
git_providers = {
"https://github.example.com/" = {
provider = "github"
}
}
}
```
@@ -125,7 +122,7 @@ To GitLab clone with a specific branch like `feat/example`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
}
@@ -137,14 +134,13 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
git_providers = {
"https://gitlab.example.com/" = {
provider = "gitlab"
}
}
}
```
@@ -159,7 +155,7 @@ For example, to clone the `feat/example` branch:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
branch_name = "feat/example"
@@ -177,7 +173,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
folder_name = "coder-dev"
@@ -195,8 +191,8 @@ If not defined, the default, `0`, performs a full clone.
```tf
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
source = "registry.coder.com/modules/git-clone/coder"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
depth = 1
@@ -212,7 +208,7 @@ This is useful for running initialization tasks like installing dependencies or
module "git-clone" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/git-clone/coder"
version = "1.2.3"
version = "1.2.2"
agent_id = coder_agent.example.id
url = "https://github.com/coder/coder"
post_clone_script = <<-EOT
+4 -5
View File
@@ -58,10 +58,9 @@ fi
# Run post-clone script if provided
if [ -n "$POST_CLONE_SCRIPT" ]; then
echo "Running post-clone script..."
POST_CLONE_TMP=$(mktemp)
echo "$POST_CLONE_SCRIPT" | base64 -d > "$POST_CLONE_TMP"
chmod +x "$POST_CLONE_TMP"
echo "$POST_CLONE_SCRIPT" | base64 -d > /tmp/post_clone.sh
chmod +x /tmp/post_clone.sh
cd "$CLONE_PATH" || exit
$POST_CLONE_TMP
rm "$POST_CLONE_TMP"
/tmp/post_clone.sh
rm /tmp/post_clone.sh
fi
+55 -50
View File
@@ -1,5 +1,9 @@
import { describe, expect, it } from "bun:test";
import {
execContainer,
findResourceInstance,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
@@ -12,66 +16,67 @@ describe("zed", async () => {
agent_id: "foo",
});
it("default output", async () => {
it("creates settings file with correct JSON", async () => {
const settings = {
theme: "One Dark",
buffer_font_size: 14,
vim_mode: true,
telemetry: {
diagnostics: false,
metrics: false,
},
// Test special characters: single quotes, backslashes, URLs
message: "it's working",
path: "C:\\Users\\test",
api_url: "https://api.example.com/v1?token=abc&user=test",
};
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
settings: JSON.stringify(settings),
});
expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder");
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "zed",
);
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer("alpine:latest");
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBeNull();
});
try {
const result = await execContainer(id, ["sh", "-c", instance.script]);
expect(result.exitCode).toBe(0);
it("adds folder", async () => {
const catResult = await execContainer(id, [
"cat",
"/root/.config/zed/settings.json",
]);
expect(catResult.exitCode).toBe(0);
const written = JSON.parse(catResult.stdout.trim());
expect(written).toEqual(settings);
} finally {
await removeContainer(id);
}
}, 30000);
it("exits early with empty settings", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/foo/bar",
});
expect(state.outputs.zed_url.value).toBe("zed://ssh/default.coder/foo/bar");
});
it("expect order to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: "22",
settings: "",
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "zed",
);
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer("alpine:latest");
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.order).toBe(22);
});
try {
const result = await execContainer(id, ["sh", "-c", instance.script]);
expect(result.exitCode).toBe(0);
it("expect display_name to be set", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
display_name: "Custom Zed",
});
const coder_app = state.resources.find(
(res) => res.type === "coder_app" && res.name === "zed",
);
expect(coder_app).not.toBeNull();
expect(coder_app?.instances.length).toBe(1);
expect(coder_app?.instances[0].attributes.display_name).toBe("Custom Zed");
});
it("adds agent_name to hostname", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
agent_name: "myagent",
});
expect(state.outputs.zed_url.value).toBe(
"zed://ssh/myagent.default.default.coder",
);
});
// Settings file should not be created
const catResult = await execContainer(id, [
"cat",
"/root/.config/zed/settings.json",
]);
expect(catResult.exitCode).not.toBe(0);
} finally {
await removeContainer(id);
}
}, 30000);
});
+4 -9
View File
@@ -65,6 +65,7 @@ locals {
owner_name = lower(data.coder_workspace_owner.me.name)
agent_name = lower(var.agent_name)
hostname = var.agent_name != "" ? "${local.agent_name}.${local.workspace_name}.${local.owner_name}.coder" : "${local.workspace_name}.coder"
settings_b64 = var.settings != "" ? base64encode(var.settings) : ""
}
resource "coder_script" "zed_settings" {
@@ -75,20 +76,14 @@ resource "coder_script" "zed_settings" {
script = <<-EOT
#!/usr/bin/env bash
set -eu
SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}'
if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then
SETTINGS_B64='${local.settings_b64}'
if [ -z "$${SETTINGS_B64}" ]; then
exit 0
fi
CONFIG_HOME="$${XDG_CONFIG_HOME:-$HOME/.config}"
ZED_DIR="$${CONFIG_HOME}/zed"
mkdir -p "$${ZED_DIR}"
SETTINGS_FILE="$${ZED_DIR}/settings.json"
if command -v jq >/dev/null 2>&1 && [ -s "$${SETTINGS_FILE}" ]; then
tmpfile="$(mktemp)"
jq -s '.[0] * .[1]' "$${SETTINGS_FILE}" <(printf '%s\n' "$${SETTINGS_JSON}") > "$${tmpfile}" && mv "$${tmpfile}" "$${SETTINGS_FILE}"
else
printf '%s\n' "$${SETTINGS_JSON}" > "$${SETTINGS_FILE}"
fi
echo -n "$${SETTINGS_B64}" | base64 -d > "$${ZED_DIR}/settings.json"
EOT
}
+74
View File
@@ -5,6 +5,20 @@ run "default_output" {
agent_id = "foo"
}
override_data {
target = data.coder_workspace.me
values = {
name = "default"
}
}
override_data {
target = data.coder_workspace_owner.me
values = {
name = "default"
}
}
assert {
condition = output.zed_url == "zed://ssh/default.coder"
error_message = "zed_url did not match expected default URL"
@@ -19,6 +33,20 @@ run "adds_folder" {
folder = "/foo/bar"
}
override_data {
target = data.coder_workspace.me
values = {
name = "default"
}
}
override_data {
target = data.coder_workspace_owner.me
values = {
name = "default"
}
}
assert {
condition = output.zed_url == "zed://ssh/default.coder/foo/bar"
error_message = "zed_url did not include provided folder path"
@@ -33,8 +61,54 @@ run "adds_agent_name" {
agent_name = "myagent"
}
override_data {
target = data.coder_workspace.me
values = {
name = "default"
}
}
override_data {
target = data.coder_workspace_owner.me
values = {
name = "default"
}
}
assert {
condition = output.zed_url == "zed://ssh/myagent.default.default.coder"
error_message = "zed_url did not include agent_name in hostname"
}
}
run "settings_base64_encoding" {
command = apply
variables {
agent_id = "foo"
settings = jsonencode({
theme = "dark"
fontSize = 14
})
}
# Verify settings are base64 encoded (eyJ = base64 prefix for JSON starting with {")
assert {
condition = can(regex("SETTINGS_B64='eyJ", coder_script.zed_settings.script))
error_message = "settings should be base64 encoded in the script"
}
}
run "empty_settings" {
command = apply
variables {
agent_id = "foo"
settings = ""
}
assert {
condition = can(regex("SETTINGS_B64=''", coder_script.zed_settings.script))
error_message = "empty settings should result in empty SETTINGS_B64"
}
}