Skip to content

Commit 5764ff2

Browse files
authored
feat: add healthcheck and config options to JupyterLab Module (#363)
## Description Simplified JupyterLab module configuration and added automatic CSP headers for iFrame embedding for Coder Tasks. The module now works out of the box without requiring users to manually configure Content-Security-Policy headers. **Changes:** - Removed redundant configuration examples from README that duplicated existing module variables - Added fallback CSP configuration when user doesn't provide custom config - Cleaned up locals logic with better naming and clearer conditionals - Updated README to show minimal usage with CSP example for custom configurations ## Type of Change - [ ] New module - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/jupyterlab` **New version:** `v1.2.0` **Breaking change:** [x] Yes [ ] No *Breaking change: Config behavior changed - now automatically includes CSP when no user config provided* ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun run fmt`) - [x] Changes tested locally ## Related Issues Closes #345
1 parent df2f432 commit 5764ff2

File tree

3 files changed

+123
-1
lines changed

3 files changed

+123
-1
lines changed

registry/coder/modules/jupyterlab/README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,32 @@ A module that adds JupyterLab in your Coder template.
1616
module "jupyterlab" {
1717
count = data.coder_workspace.me.start_count
1818
source = "registry.coder.com/coder/jupyterlab/coder"
19-
version = "1.1.1"
19+
version = "1.2.0"
2020
agent_id = coder_agent.example.id
2121
}
2222
```
23+
24+
## Configuration
25+
26+
JupyterLab is automatically configured to work with Coder's iframe embedding. For advanced configuration, you can use the `config` parameter to provide additional JupyterLab server settings according to the [JupyterLab configuration documentation](https://jupyter-server.readthedocs.io/en/latest/users/configuration.html).
27+
28+
```tf
29+
module "jupyterlab" {
30+
count = data.coder_workspace.me.start_count
31+
source = "registry.coder.com/coder/jupyterlab/coder"
32+
version = "1.2.0"
33+
agent_id = coder_agent.example.id
34+
config = {
35+
ServerApp = {
36+
# Required for Coder Tasks iFrame embedding - do not remove
37+
tornado_settings = {
38+
headers = {
39+
"Content-Security-Policy" = "frame-ancestors 'self' ${data.coder_workspace.me.access_url}"
40+
}
41+
}
42+
# Your additional configuration here
43+
root_dir = "/workspace/notebooks"
44+
}
45+
}
46+
}
47+
```

registry/coder/modules/jupyterlab/main.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
execContainer,
44
executeScriptInContainer,
55
findResourceInstance,
6+
readFileContainer,
7+
removeContainer,
68
runContainer,
79
runTerraformApply,
810
runTerraformInit,
@@ -104,4 +106,57 @@ describe("jupyterlab", async () => {
104106
// const output = await executeScriptInContainerWithPip(state, "alpine");
105107
// ...
106108
// });
109+
110+
it("writes ~/.jupyter/jupyter_server_config.json when config provided", async () => {
111+
const id = await runContainer("alpine");
112+
try {
113+
const config = {
114+
ServerApp: {
115+
port: 8888,
116+
token: "test-token",
117+
password: "",
118+
allow_origin: "*"
119+
}
120+
};
121+
const configJson = JSON.stringify(config);
122+
const state = await runTerraformApply(import.meta.dir, {
123+
agent_id: "foo",
124+
config: configJson,
125+
});
126+
const script = findResourceInstance(state, "coder_script", "jupyterlab_config").script;
127+
const resp = await execContainer(id, ["sh", "-c", script]);
128+
if (resp.exitCode !== 0) {
129+
console.log(resp.stdout);
130+
console.log(resp.stderr);
131+
}
132+
expect(resp.exitCode).toBe(0);
133+
const content = await readFileContainer(id, "/root/.jupyter/jupyter_server_config.json");
134+
// Parse both JSON strings and compare objects to avoid key ordering issues
135+
const actualConfig = JSON.parse(content);
136+
expect(actualConfig).toEqual(config);
137+
} finally {
138+
await removeContainer(id);
139+
}
140+
});
141+
142+
it("creates config script with CSP fallback when config is empty", async () => {
143+
const state = await runTerraformApply(import.meta.dir, {
144+
agent_id: "foo",
145+
config: "{}",
146+
});
147+
const configScripts = state.resources.filter(
148+
(res) => res.type === "coder_script" && res.name === "jupyterlab_config"
149+
);
150+
expect(configScripts.length).toBe(1);
151+
});
152+
153+
it("creates config script with CSP fallback when config is not provided", async () => {
154+
const state = await runTerraformApply(import.meta.dir, {
155+
agent_id: "foo",
156+
});
157+
const configScripts = state.resources.filter(
158+
(res) => res.type === "coder_script" && res.name === "jupyterlab_config"
159+
);
160+
expect(configScripts.length).toBe(1);
161+
});
107162
});

registry/coder/modules/jupyterlab/main.tf

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,23 @@ terraform {
1212
data "coder_workspace" "me" {}
1313
data "coder_workspace_owner" "me" {}
1414

15+
locals {
16+
# Fallback config with CSP for Coder iframe embedding when user config is empty
17+
csp_fallback_config = {
18+
ServerApp = {
19+
tornado_settings = {
20+
headers = {
21+
"Content-Security-Policy" = "frame-ancestors 'self' ${data.coder_workspace.me.access_url}"
22+
}
23+
}
24+
}
25+
}
26+
27+
# Use user config if provided, otherwise fallback to CSP config
28+
config_json = var.config == "{}" ? jsonencode(local.csp_fallback_config) : var.config
29+
config_b64 = base64encode(local.config_json)
30+
}
31+
1532
# Add required variables for your modules and remove any unneeded variables
1633
variable "agent_id" {
1734
type = string
@@ -57,6 +74,26 @@ variable "group" {
5774
default = null
5875
}
5976

77+
variable "config" {
78+
type = string
79+
description = "A JSON string of JupyterLab server configuration settings. When set, writes ~/.jupyter/jupyter_server_config.json."
80+
default = "{}"
81+
}
82+
83+
resource "coder_script" "jupyterlab_config" {
84+
agent_id = var.agent_id
85+
display_name = "JupyterLab Config"
86+
icon = "/icon/jupyter.svg"
87+
run_on_start = true
88+
start_blocks_login = false
89+
script = <<-EOT
90+
#!/bin/sh
91+
set -eu
92+
mkdir -p "$HOME/.jupyter"
93+
echo -n "${local.config_b64}" | base64 -d > "$HOME/.jupyter/jupyter_server_config.json"
94+
EOT
95+
}
96+
6097
resource "coder_script" "jupyterlab" {
6198
agent_id = var.agent_id
6299
display_name = "jupyterlab"
@@ -79,4 +116,9 @@ resource "coder_app" "jupyterlab" {
79116
share = var.share
80117
order = var.order
81118
group = var.group
119+
healthcheck {
120+
url = "http://localhost:${var.port}/api"
121+
interval = 5
122+
threshold = 6
123+
}
82124
}

0 commit comments

Comments
 (0)