Skip to content

Commit 704e2e2

Browse files
committed
chore: allow JupyterLab config
1 parent 4e4baa6 commit 704e2e2

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed

registry/coder/modules/jupyterlab/README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,77 @@ module "jupyterlab" {
2020
agent_id = coder_agent.example.id
2121
}
2222
```
23+
24+
## Configuration
25+
26+
You can customize JupyterLab server settings by providing a JSON configuration:
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.1.1"
33+
agent_id = coder_agent.example.id
34+
config = {
35+
ServerApp = {
36+
port = 8888
37+
token = ""
38+
password = ""
39+
allow_origin = "*"
40+
base_url = "/lab"
41+
}
42+
}
43+
}
44+
```
45+
46+
The `config` parameter accepts a map of configuration settings that will be written to `~/.jupyter/jupyter_server_config.json` before JupyterLab starts. This allows you to configure any JupyterLab server settings according to the [JupyterLab configuration documentation](https://jupyter-server.readthedocs.io/en/latest/users/configuration.html).
47+
48+
### Common Configuration Examples
49+
50+
**Disable authentication:**
51+
```tf
52+
config = {
53+
ServerApp = {
54+
token = ""
55+
password = ""
56+
}
57+
}
58+
```
59+
60+
**Set custom port and allow all origins:**
61+
```tf
62+
config = {
63+
ServerApp = {
64+
port = 9999
65+
allow_origin = "*"
66+
}
67+
}
68+
```
69+
70+
**Configure notebook directory:**
71+
```tf
72+
config = {
73+
ServerApp = {
74+
root_dir = "/workspace/notebooks"
75+
}
76+
}
77+
```
78+
79+
**Set Content-Security-Policy for iframe embedding in Coder:**
80+
```tf
81+
module "jupyterlab" {
82+
count = data.coder_workspace.me.start_count
83+
source = "registry.coder.com/coder/jupyterlab/coder"
84+
version = "1.1.1"
85+
agent_id = coder_agent.example.id
86+
config = {
87+
ServerApp = {
88+
tornado_settings = {
89+
headers = {
90+
"Content-Security-Policy" = "frame-ancestors 'self' ${data.coder_workspace.me.access_url}"
91+
}
92+
}
93+
}
94+
}
95+
}
96+
```

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

Lines changed: 53 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,55 @@ 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 expectedJson = JSON.stringify(config);
122+
const state = await runTerraformApply(import.meta.dir, {
123+
agent_id: "foo",
124+
config,
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+
expect(content).toBe(expectedJson);
135+
} finally {
136+
await removeContainer(id);
137+
}
138+
});
139+
140+
it("does not create config script when config is empty", async () => {
141+
const state = await runTerraformApply(import.meta.dir, {
142+
agent_id: "foo",
143+
config: {},
144+
});
145+
const configScripts = state.resources.filter(
146+
(res) => res.type === "coder_script" && res.name === "jupyterlab_config"
147+
);
148+
expect(configScripts.length).toBe(0);
149+
});
150+
151+
it("does not create config script when config is not provided", async () => {
152+
const state = await runTerraformApply(import.meta.dir, {
153+
agent_id: "foo",
154+
});
155+
const configScripts = state.resources.filter(
156+
(res) => res.type === "coder_script" && res.name === "jupyterlab_config"
157+
);
158+
expect(configScripts.length).toBe(0);
159+
});
107160
});

registry/coder/modules/jupyterlab/main.tf

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

15+
locals {
16+
config_json = jsonencode(var.config)
17+
config_b64 = length(var.config) > 0 ? base64encode(local.config_json) : ""
18+
}
19+
1520
# Add required variables for your modules and remove any unneeded variables
1621
variable "agent_id" {
1722
type = string
@@ -57,6 +62,27 @@ variable "group" {
5762
default = null
5863
}
5964

65+
variable "config" {
66+
type = any
67+
description = "A map of JupyterLab server configuration settings. When set, writes ~/.jupyter/jupyter_server_config.json."
68+
default = {}
69+
}
70+
71+
resource "coder_script" "jupyterlab_config" {
72+
count = length(var.config) > 0 ? 1 : 0
73+
agent_id = var.agent_id
74+
display_name = "JupyterLab Config"
75+
icon = "/icon/jupyter.svg"
76+
run_on_start = true
77+
start_blocks_login = false
78+
script = <<-EOT
79+
#!/bin/sh
80+
set -eu
81+
mkdir -p "$HOME/.jupyter"
82+
echo -n "${local.config_b64}" | base64 -d > "$HOME/.jupyter/jupyter_server_config.json"
83+
EOT
84+
}
85+
6086
resource "coder_script" "jupyterlab" {
6187
agent_id = var.agent_id
6288
display_name = "jupyterlab"

0 commit comments

Comments
 (0)