Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions registry/coder/modules/personalize/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,48 @@ tags: [helper, personalize]

# Personalize

Run a script on workspace start that allows developers to run custom commands to personalize their workspace.
Personalize runs a developer-managed script when a Coder workspace starts. It lets each developer install personal tools or configure their environment without changing the shared Coder template.

The default configuration runs `~/personalize` on workspace start and writes its output to `~/personalize.log`.

```tf
module "personalize" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/personalize/coder"
version = "1.0.32"
version = "1.0.33"
agent_id = coder_agent.main.id
}
```

## Create the personalize script

Create the script inside the workspace and make it executable. Keep startup commands idempotent because Coder runs the script on every workspace start.

```sh
cat > ~/personalize << 'EOF'
#!/usr/bin/env sh

command -v jq >/dev/null 2>&1 || echo "Install jq for this workspace"
EOF
chmod +x ~/personalize
```

If the script is missing or is not executable, Personalize prints instructions and exits without blocking the workspace with an error.

## Use custom paths

Set `path` when the script is stored elsewhere and `log_path` when its output should be written to a different location.

```tf
module "personalize" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/personalize/coder"
version = "1.0.33"
agent_id = coder_agent.main.id

path = "/home/coder/scripts/personalize workspace.sh"
log_path = "/home/coder/logs/personalize.log"
}
```

Coder creates a startup `coder_script` from this module. The script resolves the configured path, verifies that the developer's file exists and is executable, and then runs it as the workspace user. The module does not download software or require `sudo`; any network access or elevated privileges come from commands the developer places in their own script.
95 changes: 93 additions & 2 deletions registry/coder/modules/personalize/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import { describe, expect, it } from "bun:test";
import { describe, expect, it, setDefaultTimeout } from "bun:test";
import {
executeScriptInContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";

const createScript = (
path: string,
content: string,
executable = true,
): string => {
const encodedPath = Buffer.from(path).toString("base64");
const encodedContent = Buffer.from(content).toString("base64");

return [
`script_path="$(printf '%s' '${encodedPath}' | base64 -d)"`,
'mkdir -p "$(dirname "$script_path")"',
`printf '%s' '${encodedContent}' | base64 -d > "$script_path"`,
executable ? 'chmod +x "$script_path"' : "",
]
.filter(Boolean)
.join("\n");
};

setDefaultTimeout(30 * 1000);

describe("personalize", async () => {
await runTerraformInit(import.meta.dir);

Expand All @@ -22,8 +42,79 @@ describe("personalize", async () => {
expect(output.stdout).toEqual([
"✨ \u001b[0;1mYou don't have a personalize script!",
"",
"Run \u001b[36;40;1mtouch ~/personalize && chmod +x ~/personalize\u001b[0m to create one.",
"Create a script at \u001b[36;40;1m~/personalize\u001b[0m and make it executable.",
"It will run every time your workspace starts. Use it to install personal packages!",
]);
});

it("warns when the personalize script is not executable", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
const output = await executeScriptInContainer(
state,
"alpine",
"sh",
createScript(
"/root/personalize",
"#!/bin/sh\necho should-not-run\n",
false,
),
);

expect(output.exitCode).toBe(0);
expect(output.stdout).toContain(
"🔐 Your personalize script isn't executable!",
);
expect(output.stdout).not.toContain("should-not-run");
});

it("runs an executable personalize script", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
const output = await executeScriptInContainer(
state,
"alpine",
"sh",
createScript(
"/root/personalize",
"#!/bin/sh\nprintf 'personalize executed\\n'\n",
),
);

expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual(["personalize executed"]);
});

it("preserves spaces and shell syntax in a custom path", async () => {
const path = "/tmp/personalize scripts/$(printf injected) [daily]*";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
path,
});
const output = await executeScriptInContainer(
state,
"alpine",
"sh",
createScript(path, "#!/bin/sh\nprintf 'custom path executed\\n'\n"),
);

expect(output.exitCode).toBe(0);
expect(output.stdout).toEqual(["custom path executed"]);
});

it("preserves the personalize script exit code", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
const output = await executeScriptInContainer(
state,
"alpine",
"sh",
createScript("/root/personalize", "#!/bin/sh\nexit 23\n"),
);

expect(output.exitCode).toBe(23);
});
});
2 changes: 1 addition & 1 deletion registry/coder/modules/personalize/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ variable "log_path" {
resource "coder_script" "personalize" {
agent_id = var.agent_id
script = templatefile("${path.module}/run.sh", {
PERSONALIZE_PATH : var.path,
PERSONALIZE_PATH : base64encode(var.path),
})
display_name = "Personalize"
icon = "/icon/personalize.svg"
Expand Down
53 changes: 53 additions & 0 deletions registry/coder/modules/personalize/main.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
mock_provider "coder" {}

variables {
agent_id = "test-agent-id"
}

run "defaults" {
command = plan

assert {
condition = resource.coder_script.personalize.agent_id == "test-agent-id"
error_message = "Personalize must use the configured agent ID."
}

assert {
condition = resource.coder_script.personalize.log_path == "~/personalize.log"
error_message = "Personalize must preserve the default log path."
}

assert {
condition = resource.coder_script.personalize.run_on_start
error_message = "Personalize must run when the workspace starts."
}

assert {
condition = resource.coder_script.personalize.start_blocks_login
error_message = "Personalize must continue to block login until it finishes."
}
}

run "custom_paths" {
command = plan

variables {
path = "/tmp/personalize scripts/$(printf injected) [daily]*"
log_path = "/tmp/personalize logs/start.log"
}

assert {
condition = resource.coder_script.personalize.log_path == var.log_path
error_message = "Personalize must use the configured log path."
}

assert {
condition = strcontains(resource.coder_script.personalize.script, base64encode(var.path))
error_message = "The rendered script must contain the encoded custom path."
}

assert {
condition = !strcontains(resource.coder_script.personalize.script, var.path)
error_message = "The rendered script must not interpolate the raw custom path."
}
}
27 changes: 18 additions & 9 deletions registry/coder/modules/personalize/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,34 @@
BOLD='\033[0;1m'
CODE='\033[36;40;1m'
RESET='\033[0m'
SCRIPT="${PERSONALIZE_PATH}"
SCRIPT="$${SCRIPT/#\~/$${HOME}}"
# shellcheck disable=SC2016 # Terraform replaces this placeholder.
SCRIPT=$(printf '%s' '${PERSONALIZE_PATH}' | base64 -d) || {
echo "Failed to decode the personalize script path." >&2
exit 1
}
DISPLAY_PATH="$SCRIPT"
case "$SCRIPT" in
\~) SCRIPT="$${HOME}" ;;
\~/*) SCRIPT="$${HOME}/$${SCRIPT#??}" ;;
*) ;;
esac

# If the personalize script doesn't exist, educate
# the user how they can customize their environment!
if [ ! -f $SCRIPT ]; then
printf "✨ $${BOLD}You don't have a personalize script!\n\n"
printf "Run $${CODE}touch $${SCRIPT} && chmod +x $${SCRIPT}$${RESET} to create one.\n"
printf "It will run every time your workspace starts. Use it to install personal packages!\n\n"
if [ ! -f "$SCRIPT" ]; then
printf "✨ %bYou don't have a personalize script!\n\n" "$BOLD"
printf 'Create a script at %b%s%b and make it executable.\n' "$CODE" "$DISPLAY_PATH" "$RESET"
printf 'It will run every time your workspace starts. Use it to install personal packages!\n\n'
exit 0
fi

# Check if the personalize script is executable, if not,
# try to make it executable and educate the user if it fails.
if [ ! -x $SCRIPT ]; then
if [ ! -x "$SCRIPT" ]; then
echo "🔐 Your personalize script isn't executable!"
printf "Run $CODE\`chmod +x $SCRIPT\`$RESET to make it executable.\n"
printf 'Make %b%s%b executable before restarting the workspace.\n' "$CODE" "$DISPLAY_PATH" "$RESET"
exit 0
fi

# Run the personalize script!
$SCRIPT
"$SCRIPT"