Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
25 changes: 25 additions & 0 deletions registry/coder/modules/jupyterlab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,28 @@ module "jupyterlab" {
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.1.1"
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"
}
}
}
```
53 changes: 53 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 @@
execContainer,
executeScriptInContainer,
findResourceInstance,
readFileContainer,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
Expand Down Expand Up @@ -104,4 +106,55 @@
// 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);

Check failure on line 158 in registry/coder/modules/jupyterlab/main.test.ts

View workflow job for this annotation

GitHub Actions / Validate Terraform output

error: expect(received).toBe(expected)

Expected: 0 Received: 1 at <anonymous> (/home/runner/work/registry/registry/registry/coder/modules/jupyterlab/main.test.ts:158:34)
});
});
43 changes: 43 additions & 0 deletions registry/coder/modules/jupyterlab/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ 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_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
variable "agent_id" {
type = string
Expand Down Expand Up @@ -57,6 +75,26 @@ 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" {
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 +117,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
}
}
Loading