From 67e5a821d3f173490ab82284caf8c811ebcd2420 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Wed, 27 May 2026 20:24:13 -0700 Subject: [PATCH 1/5] added Docker related files for the Jupyter server --- .../src/main/resources/Dockerfile | 28 ++++++ .../src/main/resources/custom.js | 95 +++++++++++++++++++ .../src/main/resources/docker-compose.yml | 34 +++++++ 3 files changed, 157 insertions(+) create mode 100644 notebook-migration-service/src/main/resources/Dockerfile create mode 100644 notebook-migration-service/src/main/resources/custom.js create mode 100644 notebook-migration-service/src/main/resources/docker-compose.yml diff --git a/notebook-migration-service/src/main/resources/Dockerfile b/notebook-migration-service/src/main/resources/Dockerfile new file mode 100644 index 00000000000..ba64833ec03 --- /dev/null +++ b/notebook-migration-service/src/main/resources/Dockerfile @@ -0,0 +1,28 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +FROM jupyter/base-notebook:notebook-6.5.4 + +# Copy custom JavaScript for Jupyter +COPY custom.js /home/jovyan/.jupyter/custom/custom.js + +# Ensure correct permissions +USER root +RUN mkdir -p /home/jovyan/.jupyter/custom && \ + chown -R jovyan:users /home/jovyan/.jupyter + +USER jovyan \ No newline at end of file diff --git a/notebook-migration-service/src/main/resources/custom.js b/notebook-migration-service/src/main/resources/custom.js new file mode 100644 index 00000000000..789f95677ff --- /dev/null +++ b/notebook-migration-service/src/main/resources/custom.js @@ -0,0 +1,95 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Use Jupyter's event system to ensure the notebook is fully loaded +require(["base/js/events"], function (events) { + events.on("kernel_ready.Kernel", function () { + + // Attach click event listener to cells + $("#notebook-container").on("click", ".cell", function (event) { + const cell = $(this); + const index = $(".cell").index(cell); + const cellContent = cell.find(".input_area").text(); + + // Get the UUID from the cell's metadata, or use "N/A" if it doesn't exist + const cellUUID = Jupyter.notebook.get_cell(index).metadata.uuid || 'N/A'; + + // Send a message to the parent window (Texera app) + window.parent.postMessage( + { action: "cellClicked", cellIndex: index, cellContent: cellContent, cellUUID: cellUUID }, + "http://localhost:4200" + ); + }); + }); +}); + +// Listen for messages from the Texera app (or parent window) +window.addEventListener("message", function (event) { + // Verify the message origin + if (event.origin !== 'http://localhost:4200') { + console.warn("Message received from unrecognized origin:", event.origin); + return; + } + + if (event.data.action === "triggerCellClick") { + const operatorCellUUIDs = event.data.operators || []; + + if (!operatorCellUUIDs.length) { + console.error("No valid operator UUIDs provided in the message."); + return; // Exit if no UUIDs are provided + } + + operatorCellUUIDs.forEach((cellUUID) => { + // Search for the cell by UUID + const allCells = Jupyter.notebook.get_cells(); + const targetCell = allCells.find((cell) => cell.metadata.uuid === cellUUID); + + if (targetCell) { + const cellIndex = Jupyter.notebook.find_cell_index(targetCell); + + // Scroll to and highlight the cell + let cell = document.querySelectorAll(".cell")[cellIndex]; + if (cell) { + cell.scrollIntoView({ behavior: 'smooth', block: 'center' }); + cell.classList.add("highlighted"); + + // Remove the highlight after 3 seconds + setTimeout(() => { + cell.classList.remove("highlighted"); + }, 3000); + } else { + console.error(`Cell not found in the DOM for index ${cellIndex}.`); + } + } else { + console.error(`No cell found with UUID: ${cellUUID}`); + } + }); + } else { + console.warn("Received unknown action:", event.data.action); + } +}, false); + +// Add custom CSS for highlighted cells +const style = document.createElement('style'); +style.innerHTML = ` + .cell.highlighted { + background-color: lightyellow; + } +`; +document.head.appendChild(style); diff --git a/notebook-migration-service/src/main/resources/docker-compose.yml b/notebook-migration-service/src/main/resources/docker-compose.yml new file mode 100644 index 00000000000..dc10d5bc980 --- /dev/null +++ b/notebook-migration-service/src/main/resources/docker-compose.yml @@ -0,0 +1,34 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +name: texera-jupyter +services: + + jupyter: + build: + context: . + dockerfile: Dockerfile + container_name: texera-jupyter + ports: + - "9100:8888" + command: > + start-notebook.sh + --NotebookApp.token='' + --NotebookApp.password='' + --NotebookApp.disable_check_xsrf=True + --NotebookApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors http://localhost:*'}}" + --NotebookApp.default_url=/tree \ No newline at end of file From c7b767196ae5e4f28d31706efe59d6099a4c35ea Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 13:39:09 -0700 Subject: [PATCH 2/5] prevent duplicate cell click handlers on kernel restart --- notebook-migration-service/src/main/resources/custom.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/notebook-migration-service/src/main/resources/custom.js b/notebook-migration-service/src/main/resources/custom.js index 789f95677ff..d3f1bb4565d 100644 --- a/notebook-migration-service/src/main/resources/custom.js +++ b/notebook-migration-service/src/main/resources/custom.js @@ -21,8 +21,10 @@ require(["base/js/events"], function (events) { events.on("kernel_ready.Kernel", function () { - // Attach click event listener to cells - $("#notebook-container").on("click", ".cell", function (event) { + // Attach click event listener to cells. kernel_ready.Kernel fires on every + // kernel (re)start, so remove any previously bound handler first to avoid + // stacking duplicate listeners that would post N messages per click. + $("#notebook-container").off("click", ".cell").on("click", ".cell", function (event) { const cell = $(this); const index = $(".cell").index(cell); const cellContent = cell.find(".input_area").text(); From ce773367f260587ae27e303b32ac644fba04ebb0 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 13:42:41 -0700 Subject: [PATCH 3/5] add restart policy and healthcheck to jupyter service --- .../src/main/resources/docker-compose.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/notebook-migration-service/src/main/resources/docker-compose.yml b/notebook-migration-service/src/main/resources/docker-compose.yml index dc10d5bc980..c37abf62ccd 100644 --- a/notebook-migration-service/src/main/resources/docker-compose.yml +++ b/notebook-migration-service/src/main/resources/docker-compose.yml @@ -23,6 +23,7 @@ services: context: . dockerfile: Dockerfile container_name: texera-jupyter + restart: unless-stopped ports: - "9100:8888" command: > @@ -31,4 +32,12 @@ services: --NotebookApp.password='' --NotebookApp.disable_check_xsrf=True --NotebookApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors http://localhost:*'}}" - --NotebookApp.default_url=/tree \ No newline at end of file + --NotebookApp.default_url=/tree + healthcheck: + # /api returns the server version without requiring the token, so it is a + # reliable liveness probe even with auth enabled. + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8888/api')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s \ No newline at end of file From 288c3cc5bb148a4a30fffa4fb6efb4516489b7a0 Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 13:46:04 -0700 Subject: [PATCH 4/5] make texera origin configurable via TEXERA_ORIGIN env var --- .../src/main/resources/Dockerfile | 11 +++--- .../src/main/resources/custom.js | 10 ++++-- .../src/main/resources/docker-compose.yml | 12 +++---- .../main/resources/start-texera-jupyter.sh | 34 +++++++++++++++++++ 4 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 notebook-migration-service/src/main/resources/start-texera-jupyter.sh diff --git a/notebook-migration-service/src/main/resources/Dockerfile b/notebook-migration-service/src/main/resources/Dockerfile index ba64833ec03..699b2719431 100644 --- a/notebook-migration-service/src/main/resources/Dockerfile +++ b/notebook-migration-service/src/main/resources/Dockerfile @@ -17,12 +17,15 @@ FROM jupyter/base-notebook:notebook-6.5.4 -# Copy custom JavaScript for Jupyter +# Copy custom JavaScript for Jupyter and the startup script COPY custom.js /home/jovyan/.jupyter/custom/custom.js +COPY start-texera-jupyter.sh /usr/local/bin/start-texera-jupyter.sh -# Ensure correct permissions +# Ensure correct permissions. custom.js must stay writable by jovyan so the +# startup script can substitute the origin placeholder at runtime. USER root RUN mkdir -p /home/jovyan/.jupyter/custom && \ - chown -R jovyan:users /home/jovyan/.jupyter + chown -R jovyan:users /home/jovyan/.jupyter && \ + chmod +x /usr/local/bin/start-texera-jupyter.sh -USER jovyan \ No newline at end of file +USER jovyan diff --git a/notebook-migration-service/src/main/resources/custom.js b/notebook-migration-service/src/main/resources/custom.js index d3f1bb4565d..70fc4ce931e 100644 --- a/notebook-migration-service/src/main/resources/custom.js +++ b/notebook-migration-service/src/main/resources/custom.js @@ -17,6 +17,12 @@ * under the License. */ +// The Texera app origin. The "__TEXERA_ORIGIN__" placeholder is substituted at +// container startup by start-texera-jupyter.sh from the TEXERA_ORIGIN env var +// (defaults to http://localhost:4200), so deployments under a real hostname work +// without editing this file. +const TEXERA_ORIGIN = "__TEXERA_ORIGIN__"; + // Use Jupyter's event system to ensure the notebook is fully loaded require(["base/js/events"], function (events) { events.on("kernel_ready.Kernel", function () { @@ -35,7 +41,7 @@ require(["base/js/events"], function (events) { // Send a message to the parent window (Texera app) window.parent.postMessage( { action: "cellClicked", cellIndex: index, cellContent: cellContent, cellUUID: cellUUID }, - "http://localhost:4200" + TEXERA_ORIGIN ); }); }); @@ -44,7 +50,7 @@ require(["base/js/events"], function (events) { // Listen for messages from the Texera app (or parent window) window.addEventListener("message", function (event) { // Verify the message origin - if (event.origin !== 'http://localhost:4200') { + if (event.origin !== TEXERA_ORIGIN) { console.warn("Message received from unrecognized origin:", event.origin); return; } diff --git a/notebook-migration-service/src/main/resources/docker-compose.yml b/notebook-migration-service/src/main/resources/docker-compose.yml index c37abf62ccd..133dd583998 100644 --- a/notebook-migration-service/src/main/resources/docker-compose.yml +++ b/notebook-migration-service/src/main/resources/docker-compose.yml @@ -26,13 +26,11 @@ services: restart: unless-stopped ports: - "9100:8888" - command: > - start-notebook.sh - --NotebookApp.token='' - --NotebookApp.password='' - --NotebookApp.disable_check_xsrf=True - --NotebookApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors http://localhost:*'}}" - --NotebookApp.default_url=/tree + environment: + # Texera app origin, used for the iframe CSP frame-ancestors and the + # postMessage origin checks in custom.js. Override for non-local deployments. + - TEXERA_ORIGIN=http://localhost:4200 + command: ["start-texera-jupyter.sh"] healthcheck: # /api returns the server version without requiring the token, so it is a # reliable liveness probe even with auth enabled. diff --git a/notebook-migration-service/src/main/resources/start-texera-jupyter.sh b/notebook-migration-service/src/main/resources/start-texera-jupyter.sh new file mode 100644 index 00000000000..1df245bcd25 --- /dev/null +++ b/notebook-migration-service/src/main/resources/start-texera-jupyter.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set -euo pipefail + +# Texera app origin used by custom.js (postMessage targetOrigin + inbound origin +# check) and by the iframe CSP frame-ancestors. Override TEXERA_ORIGIN for +# deployments under a real hostname; defaults to the local dev origin. +TEXERA_ORIGIN="${TEXERA_ORIGIN:-http://localhost:4200}" + +# Substitute the origin placeholder in custom.js before the server starts serving it. +sed -i "s|__TEXERA_ORIGIN__|${TEXERA_ORIGIN}|g" /home/jovyan/.jupyter/custom/custom.js + +exec start-notebook.sh \ + --NotebookApp.token='' \ + --NotebookApp.password='' \ + --NotebookApp.disable_check_xsrf=True \ + --NotebookApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors ${TEXERA_ORIGIN}'}}" \ + --NotebookApp.default_url=/tree From f0c16243728ad5a3136f90327772bcd91a66634b Mon Sep 17 00:00:00 2001 From: Ryan Zhang Date: Tue, 16 Jun 2026 13:46:29 -0700 Subject: [PATCH 5/5] require a token to access the jupyter server --- .../src/main/resources/docker-compose.yml | 5 ++++- .../src/main/resources/start-texera-jupyter.sh | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/notebook-migration-service/src/main/resources/docker-compose.yml b/notebook-migration-service/src/main/resources/docker-compose.yml index 133dd583998..d442a573866 100644 --- a/notebook-migration-service/src/main/resources/docker-compose.yml +++ b/notebook-migration-service/src/main/resources/docker-compose.yml @@ -30,6 +30,9 @@ services: # Texera app origin, used for the iframe CSP frame-ancestors and the # postMessage origin checks in custom.js. Override for non-local deployments. - TEXERA_ORIGIN=http://localhost:4200 + # Weak default token so the server is not fully open. The Texera-side iframe + # URL must pass this through ?token=. + - JUPYTER_TOKEN=texera command: ["start-texera-jupyter.sh"] healthcheck: # /api returns the server version without requiring the token, so it is a @@ -38,4 +41,4 @@ services: interval: 10s timeout: 5s retries: 5 - start_period: 15s \ No newline at end of file + start_period: 15s diff --git a/notebook-migration-service/src/main/resources/start-texera-jupyter.sh b/notebook-migration-service/src/main/resources/start-texera-jupyter.sh index 1df245bcd25..2bfb5a3baff 100644 --- a/notebook-migration-service/src/main/resources/start-texera-jupyter.sh +++ b/notebook-migration-service/src/main/resources/start-texera-jupyter.sh @@ -23,11 +23,15 @@ set -euo pipefail # deployments under a real hostname; defaults to the local dev origin. TEXERA_ORIGIN="${TEXERA_ORIGIN:-http://localhost:4200}" +# Weak default token so the server is not fully open to anyone reachable on the +# published port. The Texera-side iframe URL must pass this through ?token=. +JUPYTER_TOKEN="${JUPYTER_TOKEN:-texera}" + # Substitute the origin placeholder in custom.js before the server starts serving it. sed -i "s|__TEXERA_ORIGIN__|${TEXERA_ORIGIN}|g" /home/jovyan/.jupyter/custom/custom.js exec start-notebook.sh \ - --NotebookApp.token='' \ + --NotebookApp.token="${JUPYTER_TOKEN}" \ --NotebookApp.password='' \ --NotebookApp.disable_check_xsrf=True \ --NotebookApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors ${TEXERA_ORIGIN}'}}" \