Skip to content
Merged
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
27 changes: 26 additions & 1 deletion registry/coder/modules/jupyterlab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,32 @@ 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
}
```

## Configuration

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).

```tf
module "jupyterlab" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/jupyterlab/coder"
version = "1.2.0"
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"
}
}
}
```
55 changes: 55 additions & 0 deletions registry/coder/modules/jupyterlab/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
execContainer,
executeScriptInContainer,
findResourceInstance,
readFileContainer,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
Expand Down Expand Up @@ -104,4 +106,57 @@ 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 configJson = JSON.stringify(config);
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
config: configJson,
});
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");
// 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);
}
});

it("creates config script with CSP fallback 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(1);
});

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(1);
});
});
42 changes: 42 additions & 0 deletions registry/coder/modules/jupyterlab/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ terraform {
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}

locals {
# 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_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
variable "agent_id" {
type = string
Expand Down Expand Up @@ -57,6 +74,26 @@ variable "group" {
default = null
}

variable "config" {
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" {
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"
Expand All @@ -79,4 +116,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
}
}