From 4e4baa66e4db03a48b8ef5879e74c5d37ebfdbe0 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Fri, 22 Aug 2025 17:39:18 +0000 Subject: [PATCH 1/9] chore: add health check to jupyerlab --- registry/coder/modules/jupyterlab/main.tf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/registry/coder/modules/jupyterlab/main.tf b/registry/coder/modules/jupyterlab/main.tf index 1237d980d..4e86f3c00 100644 --- a/registry/coder/modules/jupyterlab/main.tf +++ b/registry/coder/modules/jupyterlab/main.tf @@ -79,4 +79,9 @@ resource "coder_app" "jupyterlab" { share = var.share order = var.order group = var.group + healthcheck { + url = "http://localhost:${var.port}/api" + interval = 5 + threshold = 6 + } } From 704e2e20bebc5700a2b36a37d8cfb22884963367 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Fri, 22 Aug 2025 18:13:52 +0000 Subject: [PATCH 2/9] chore: allow JupyterLab config --- registry/coder/modules/jupyterlab/README.md | 74 +++++++++++++++++++ .../coder/modules/jupyterlab/main.test.ts | 53 +++++++++++++ registry/coder/modules/jupyterlab/main.tf | 26 +++++++ 3 files changed, 153 insertions(+) diff --git a/registry/coder/modules/jupyterlab/README.md b/registry/coder/modules/jupyterlab/README.md index ed7400dca..f2c6d1728 100644 --- a/registry/coder/modules/jupyterlab/README.md +++ b/registry/coder/modules/jupyterlab/README.md @@ -20,3 +20,77 @@ module "jupyterlab" { agent_id = coder_agent.example.id } ``` + +## Configuration + +You can customize JupyterLab server settings by providing a JSON configuration: + +```tf +module "jupyterlab" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jupyterlab/coder" + version = "1.1.1" + agent_id = coder_agent.example.id + config = { + ServerApp = { + port = 8888 + token = "" + password = "" + allow_origin = "*" + base_url = "/lab" + } + } +} +``` + +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). + +### Common Configuration Examples + +**Disable authentication:** +```tf +config = { + ServerApp = { + token = "" + password = "" + } +} +``` + +**Set custom port and allow all origins:** +```tf +config = { + ServerApp = { + port = 9999 + allow_origin = "*" + } +} +``` + +**Configure notebook directory:** +```tf +config = { + ServerApp = { + root_dir = "/workspace/notebooks" + } +} +``` + +**Set Content-Security-Policy for iframe embedding in Coder:** +```tf +module "jupyterlab" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/jupyterlab/coder" + version = "1.1.1" + agent_id = coder_agent.example.id + config = { + ServerApp = { + tornado_settings = { + headers = { + "Content-Security-Policy" = "frame-ancestors 'self' ${data.coder_workspace.me.access_url}" + } + } + } + } +} +``` diff --git a/registry/coder/modules/jupyterlab/main.test.ts b/registry/coder/modules/jupyterlab/main.test.ts index 4ef7fa026..bf5a99033 100644 --- a/registry/coder/modules/jupyterlab/main.test.ts +++ b/registry/coder/modules/jupyterlab/main.test.ts @@ -3,6 +3,8 @@ import { execContainer, executeScriptInContainer, findResourceInstance, + readFileContainer, + removeContainer, runContainer, runTerraformApply, runTerraformInit, @@ -104,4 +106,55 @@ describe("jupyterlab", async () => { // const output = await executeScriptInContainerWithPip(state, "alpine"); // ... // }); + + it("writes ~/.jupyter/jupyter_server_config.json when config provided", async () => { + const id = await runContainer("alpine"); + try { + const config = { + ServerApp: { + port: 8888, + token: "test-token", + password: "", + allow_origin: "*" + } + }; + const expectedJson = JSON.stringify(config); + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + config, + }); + const script = findResourceInstance(state, "coder_script", "jupyterlab_config").script; + const resp = await execContainer(id, ["sh", "-c", script]); + if (resp.exitCode !== 0) { + console.log(resp.stdout); + console.log(resp.stderr); + } + expect(resp.exitCode).toBe(0); + const content = await readFileContainer(id, "/root/.jupyter/jupyter_server_config.json"); + expect(content).toBe(expectedJson); + } finally { + await removeContainer(id); + } + }); + + it("does not create config script when config is empty", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + config: {}, + }); + const configScripts = state.resources.filter( + (res) => res.type === "coder_script" && res.name === "jupyterlab_config" + ); + expect(configScripts.length).toBe(0); + }); + + it("does not create config script when config is not provided", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + const configScripts = state.resources.filter( + (res) => res.type === "coder_script" && res.name === "jupyterlab_config" + ); + expect(configScripts.length).toBe(0); + }); }); diff --git a/registry/coder/modules/jupyterlab/main.tf b/registry/coder/modules/jupyterlab/main.tf index 4e86f3c00..7a58c62de 100644 --- a/registry/coder/modules/jupyterlab/main.tf +++ b/registry/coder/modules/jupyterlab/main.tf @@ -12,6 +12,11 @@ terraform { data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} +locals { + config_json = jsonencode(var.config) + config_b64 = length(var.config) > 0 ? base64encode(local.config_json) : "" +} + # Add required variables for your modules and remove any unneeded variables variable "agent_id" { type = string @@ -57,6 +62,27 @@ variable "group" { default = null } +variable "config" { + type = any + description = "A map of JupyterLab server configuration settings. When set, writes ~/.jupyter/jupyter_server_config.json." + default = {} +} + +resource "coder_script" "jupyterlab_config" { + count = length(var.config) > 0 ? 1 : 0 + agent_id = var.agent_id + display_name = "JupyterLab Config" + icon = "/icon/jupyter.svg" + run_on_start = true + start_blocks_login = false + script = <<-EOT + #!/bin/sh + set -eu + mkdir -p "$HOME/.jupyter" + echo -n "${local.config_b64}" | base64 -d > "$HOME/.jupyter/jupyter_server_config.json" + EOT +} + resource "coder_script" "jupyterlab" { agent_id = var.agent_id display_name = "jupyterlab" From d3ee450641c471484f23e688ce70bbbdce9b6d7f Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sat, 23 Aug 2025 02:19:40 +0000 Subject: [PATCH 3/9] simplify README --- registry/coder/modules/jupyterlab/README.md | 49 +-------------------- 1 file changed, 2 insertions(+), 47 deletions(-) diff --git a/registry/coder/modules/jupyterlab/README.md b/registry/coder/modules/jupyterlab/README.md index f2c6d1728..fc629737f 100644 --- a/registry/coder/modules/jupyterlab/README.md +++ b/registry/coder/modules/jupyterlab/README.md @@ -25,56 +25,11 @@ module "jupyterlab" { You can customize JupyterLab server settings by providing a JSON configuration: -```tf -module "jupyterlab" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/jupyterlab/coder" - version = "1.1.1" - agent_id = coder_agent.example.id - config = { - ServerApp = { - port = 8888 - token = "" - password = "" - allow_origin = "*" - base_url = "/lab" - } - } -} -``` - 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). -### Common Configuration Examples +### Frame Embedding Configuration -**Disable authentication:** -```tf -config = { - ServerApp = { - token = "" - password = "" - } -} -``` - -**Set custom port and allow all origins:** -```tf -config = { - ServerApp = { - port = 9999 - allow_origin = "*" - } -} -``` - -**Configure notebook directory:** -```tf -config = { - ServerApp = { - root_dir = "/workspace/notebooks" - } -} -``` +To allow JupyterLab to be embedded in Coder's iframe: **Set Content-Security-Policy for iframe embedding in Coder:** ```tf From dd69d7023c7392c62edad3b210596262da5bea80 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sat, 23 Aug 2025 02:34:47 +0000 Subject: [PATCH 4/9] clear up config --- registry/coder/modules/jupyterlab/README.md | 14 +++++--------- registry/coder/modules/jupyterlab/main.tf | 18 +++++++++++++++--- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/registry/coder/modules/jupyterlab/README.md b/registry/coder/modules/jupyterlab/README.md index fc629737f..4d6247ac7 100644 --- a/registry/coder/modules/jupyterlab/README.md +++ b/registry/coder/modules/jupyterlab/README.md @@ -1,4 +1,4 @@ ---- +o--- display_name: JupyterLab description: A module that adds JupyterLab in your Coder template. icon: ../../../../.icons/jupyter.svg @@ -23,15 +23,8 @@ module "jupyterlab" { ## Configuration -You can customize JupyterLab server settings by providing a JSON configuration: - -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). - -### Frame Embedding Configuration - -To allow JupyterLab to be embedded in Coder's iframe: +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). -**Set Content-Security-Policy for iframe embedding in Coder:** ```tf module "jupyterlab" { count = data.coder_workspace.me.start_count @@ -40,11 +33,14 @@ module "jupyterlab" { agent_id = coder_agent.example.id config = { ServerApp = { + # Required for Coder Tasks iFrame embedding - do not remove tornado_settings = { headers = { "Content-Security-Policy" = "frame-ancestors 'self' ${data.coder_workspace.me.access_url}" } } + # Your additional configuration here + root_dir = "/workspace/notebooks" } } } diff --git a/registry/coder/modules/jupyterlab/main.tf b/registry/coder/modules/jupyterlab/main.tf index 7a58c62de..3a263a458 100644 --- a/registry/coder/modules/jupyterlab/main.tf +++ b/registry/coder/modules/jupyterlab/main.tf @@ -13,8 +13,21 @@ data "coder_workspace" "me" {} data "coder_workspace_owner" "me" {} locals { - config_json = jsonencode(var.config) - config_b64 = length(var.config) > 0 ? base64encode(local.config_json) : "" + # Fallback config with CSP for Coder iframe embedding when user config is empty + csp_fallback_config = { + ServerApp = { + tornado_settings = { + headers = { + "Content-Security-Policy" = "frame-ancestors 'self' ${data.coder_workspace.me.access_url}" + } + } + } + } + + # Use user config if provided, otherwise fallback to CSP config + config_to_use = length(var.config) == 0 ? local.csp_fallback_config : var.config + config_json = jsonencode(local.config_to_use) + config_b64 = base64encode(local.config_json) } # Add required variables for your modules and remove any unneeded variables @@ -69,7 +82,6 @@ variable "config" { } resource "coder_script" "jupyterlab_config" { - count = length(var.config) > 0 ? 1 : 0 agent_id = var.agent_id display_name = "JupyterLab Config" icon = "/icon/jupyter.svg" From 128b0b3ad8159ca386a7acaf98fb4d62bab7a759 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sat, 23 Aug 2025 02:48:05 +0000 Subject: [PATCH 5/9] fmt --- registry/coder/modules/jupyterlab/main.tf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/coder/modules/jupyterlab/main.tf b/registry/coder/modules/jupyterlab/main.tf index 3a263a458..799b89e9a 100644 --- a/registry/coder/modules/jupyterlab/main.tf +++ b/registry/coder/modules/jupyterlab/main.tf @@ -23,11 +23,11 @@ locals { } } } - + # Use user config if provided, otherwise fallback to CSP config config_to_use = length(var.config) == 0 ? local.csp_fallback_config : var.config - config_json = jsonencode(local.config_to_use) - config_b64 = base64encode(local.config_json) + config_json = jsonencode(local.config_to_use) + config_b64 = base64encode(local.config_json) } # Add required variables for your modules and remove any unneeded variables From 8eacc91f87a16aa2b40aade48ba0fede5a48bbec Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sat, 23 Aug 2025 02:48:50 +0000 Subject: [PATCH 6/9] fix frontmatter typo --- registry/coder/modules/jupyterlab/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/jupyterlab/README.md b/registry/coder/modules/jupyterlab/README.md index 4d6247ac7..e289c6541 100644 --- a/registry/coder/modules/jupyterlab/README.md +++ b/registry/coder/modules/jupyterlab/README.md @@ -1,4 +1,4 @@ -o--- +--- display_name: JupyterLab description: A module that adds JupyterLab in your Coder template. icon: ../../../../.icons/jupyter.svg From 7cff16a7f10db3d400a86356fa60c45a46bef67f Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sat, 23 Aug 2025 14:21:34 +0000 Subject: [PATCH 7/9] chore: bump module versions (minor) --- registry/coder/modules/jupyterlab/README.md | 4 ++-- registry/coder/modules/jupyterlab/main.test.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/jupyterlab/README.md b/registry/coder/modules/jupyterlab/README.md index e289c6541..3550da4c8 100644 --- a/registry/coder/modules/jupyterlab/README.md +++ b/registry/coder/modules/jupyterlab/README.md @@ -16,7 +16,7 @@ A module that adds JupyterLab in your Coder template. module "jupyterlab" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jupyterlab/coder" - version = "1.1.1" + version = "1.2.0" agent_id = coder_agent.example.id } ``` @@ -29,7 +29,7 @@ JupyterLab is automatically configured to work with Coder's iframe embedding. Fo module "jupyterlab" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jupyterlab/coder" - version = "1.1.1" + version = "1.2.0" agent_id = coder_agent.example.id config = { ServerApp = { diff --git a/registry/coder/modules/jupyterlab/main.test.ts b/registry/coder/modules/jupyterlab/main.test.ts index bf5a99033..2adc8d9ab 100644 --- a/registry/coder/modules/jupyterlab/main.test.ts +++ b/registry/coder/modules/jupyterlab/main.test.ts @@ -137,7 +137,7 @@ describe("jupyterlab", async () => { } }); - it("does not create config script when config is empty", async () => { + it("creates config script with CSP fallback when config is empty", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", config: {}, @@ -145,16 +145,16 @@ describe("jupyterlab", async () => { const configScripts = state.resources.filter( (res) => res.type === "coder_script" && res.name === "jupyterlab_config" ); - expect(configScripts.length).toBe(0); + expect(configScripts.length).toBe(1); }); - it("does not create config script when config is not provided", async () => { + it("creates config script with CSP fallback when config is not provided", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", }); const configScripts = state.resources.filter( (res) => res.type === "coder_script" && res.name === "jupyterlab_config" ); - expect(configScripts.length).toBe(0); + expect(configScripts.length).toBe(1); }); }); From 8fa74fb8bfae9c40c378d92130c841b98f589987 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sat, 23 Aug 2025 14:37:34 +0000 Subject: [PATCH 8/9] fix test structure --- registry/coder/modules/jupyterlab/main.test.ts | 10 ++++++---- registry/coder/modules/jupyterlab/main.tf | 11 +++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/registry/coder/modules/jupyterlab/main.test.ts b/registry/coder/modules/jupyterlab/main.test.ts index 2adc8d9ab..06caff3aa 100644 --- a/registry/coder/modules/jupyterlab/main.test.ts +++ b/registry/coder/modules/jupyterlab/main.test.ts @@ -118,10 +118,10 @@ describe("jupyterlab", async () => { allow_origin: "*" } }; - const expectedJson = JSON.stringify(config); + const configJson = JSON.stringify(config); const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", - config, + config: configJson, }); const script = findResourceInstance(state, "coder_script", "jupyterlab_config").script; const resp = await execContainer(id, ["sh", "-c", script]); @@ -131,7 +131,9 @@ describe("jupyterlab", async () => { } expect(resp.exitCode).toBe(0); const content = await readFileContainer(id, "/root/.jupyter/jupyter_server_config.json"); - expect(content).toBe(expectedJson); + // Parse both JSON strings and compare objects to avoid key ordering issues + const actualConfig = JSON.parse(content); + expect(actualConfig).toEqual(config); } finally { await removeContainer(id); } @@ -140,7 +142,7 @@ describe("jupyterlab", async () => { it("creates config script with CSP fallback when config is empty", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", - config: {}, + config: "{}", }); const configScripts = state.resources.filter( (res) => res.type === "coder_script" && res.name === "jupyterlab_config" diff --git a/registry/coder/modules/jupyterlab/main.tf b/registry/coder/modules/jupyterlab/main.tf index 799b89e9a..f2b308608 100644 --- a/registry/coder/modules/jupyterlab/main.tf +++ b/registry/coder/modules/jupyterlab/main.tf @@ -25,9 +25,8 @@ locals { } # Use user config if provided, otherwise fallback to CSP config - config_to_use = length(var.config) == 0 ? local.csp_fallback_config : var.config - config_json = jsonencode(local.config_to_use) - config_b64 = base64encode(local.config_json) + config_json = var.config == "{}" ? jsonencode(local.csp_fallback_config) : var.config + config_b64 = base64encode(local.config_json) } # Add required variables for your modules and remove any unneeded variables @@ -76,9 +75,9 @@ variable "group" { } variable "config" { - type = any - description = "A map of JupyterLab server configuration settings. When set, writes ~/.jupyter/jupyter_server_config.json." - default = {} + type = string + description = "A JSON string of JupyterLab server configuration settings. When set, writes ~/.jupyter/jupyter_server_config.json." + default = "{}" } resource "coder_script" "jupyterlab_config" { From 3d938951c6febb48956d3faf0615df2e960200c0 Mon Sep 17 00:00:00 2001 From: Ben Potter Date: Sat, 23 Aug 2025 14:41:20 +0000 Subject: [PATCH 9/9] fmt --- registry/coder/modules/jupyterlab/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/jupyterlab/README.md b/registry/coder/modules/jupyterlab/README.md index 3550da4c8..6c401dede 100644 --- a/registry/coder/modules/jupyterlab/README.md +++ b/registry/coder/modules/jupyterlab/README.md @@ -16,7 +16,7 @@ A module that adds JupyterLab in your Coder template. module "jupyterlab" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jupyterlab/coder" - version = "1.2.0" + version = "1.2.0" agent_id = coder_agent.example.id } ``` @@ -29,7 +29,7 @@ JupyterLab is automatically configured to work with Coder's iframe embedding. Fo module "jupyterlab" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jupyterlab/coder" - version = "1.2.0" + version = "1.2.0" agent_id = coder_agent.example.id config = { ServerApp = {