diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml index 1b11bcb..8fb41d7 100644 --- a/.github/workflows/build-image.yaml +++ b/.github/workflows/build-image.yaml @@ -37,6 +37,11 @@ on: required: false default: "" type: string + smoke_test_labextensions: + description: "After each arch build, run `jupyter labextension list` in the pushed image and assert the extensions pinned in images/jupyterlab/pixi.toml are enabled at their pinned versions. Enable for every image built from the jupyterlab Dockerfile target (including the GPU variant — it installs the same pixi.toml). Skipped on fork PRs (no pushed image to run)." + required: false + default: false + type: boolean secrets: QUAY_TOKEN: required: false @@ -96,6 +101,36 @@ jobs: GPU=${{ inputs.gpu }} NEBI_IMAGE=${{ inputs.nebi_image }} + - name: "Smoke test: labextensions enabled at pinned versions" + if: env.IS_FORK_PR != 'true' && inputs.smoke_test_labextensions + run: | + ref="ghcr.io/${{ github.repository_owner }}/${{ inputs.image }}@${{ steps.build.outputs.digest }}" + # `jupyter labextension list` writes to stderr and colorizes the status + # words unconditionally (jupyter_server.extension.serverextension + # GREEN_ENABLED / RED_DISABLED / RED_X — guarded only by os.name != "nt"), + # so merge stderr and strip SGR sequences before matching. Keep grep -F + # substring matching: the real line has a trailing "(python, )" and + # may carry "*" or a lock emoji after OK. + if ! out=$(docker run --rm "$ref" jupyter labextension list 2>&1); then + printf '%s\n' "$out" + echo "::error::could not run 'jupyter labextension list' in $ref" + exit 1 + fi + plain=$(printf '%s\n' "$out" | sed -E 's/\x1b\[[0-9;]*m//g') + printf '%s\n' "$plain" + # Extensions whose pixi.toml pin name matches their labextension name. + # The list is hardcoded rather than derived from pixi.toml because the + # mapping isn't mechanical (jupyterlab_nvdashboard ships the extension + # as jupyterlab-nvdashboard; jhub-apps isn't a labextension at all). + for ext in jupyterlab-launchpad jupyterlab-nebari-mode jupyterlab-gallery jupyterlab-jhub-apps; do + expected=$(sed -nE "s/^${ext} = \"==([^\"]+)\"\$/\\1/p" images/jupyterlab/pixi.toml) + test -n "$expected" || { echo "::error::could not read exact ${ext} pin from images/jupyterlab/pixi.toml"; exit 1; } + grep -Fq " ${ext} v${expected} enabled OK" <<< "$plain" || { + echo "::error::${ext} v${expected} is not 'enabled OK' in $ref" + exit 1 + } + done + - name: "Export digest" if: env.IS_FORK_PR != 'true' run: | @@ -150,6 +185,36 @@ jobs: GPU=${{ inputs.gpu }} NEBI_IMAGE=${{ inputs.nebi_image }} + - name: "Smoke test: labextensions enabled at pinned versions" + if: env.IS_FORK_PR != 'true' && inputs.smoke_test_labextensions + run: | + ref="ghcr.io/${{ github.repository_owner }}/${{ inputs.image }}@${{ steps.build.outputs.digest }}" + # `jupyter labextension list` writes to stderr and colorizes the status + # words unconditionally (jupyter_server.extension.serverextension + # GREEN_ENABLED / RED_DISABLED / RED_X — guarded only by os.name != "nt"), + # so merge stderr and strip SGR sequences before matching. Keep grep -F + # substring matching: the real line has a trailing "(python, )" and + # may carry "*" or a lock emoji after OK. + if ! out=$(docker run --rm "$ref" jupyter labextension list 2>&1); then + printf '%s\n' "$out" + echo "::error::could not run 'jupyter labextension list' in $ref" + exit 1 + fi + plain=$(printf '%s\n' "$out" | sed -E 's/\x1b\[[0-9;]*m//g') + printf '%s\n' "$plain" + # Extensions whose pixi.toml pin name matches their labextension name. + # The list is hardcoded rather than derived from pixi.toml because the + # mapping isn't mechanical (jupyterlab_nvdashboard ships the extension + # as jupyterlab-nvdashboard; jhub-apps isn't a labextension at all). + for ext in jupyterlab-launchpad jupyterlab-nebari-mode jupyterlab-gallery jupyterlab-jhub-apps; do + expected=$(sed -nE "s/^${ext} = \"==([^\"]+)\"\$/\\1/p" images/jupyterlab/pixi.toml) + test -n "$expected" || { echo "::error::could not read exact ${ext} pin from images/jupyterlab/pixi.toml"; exit 1; } + grep -Fq " ${ext} v${expected} enabled OK" <<< "$plain" || { + echo "::error::${ext} v${expected} is not 'enabled OK' in $ref" + exit 1 + } + done + - name: "Export digest" if: env.IS_FORK_PR != 'true' run: | diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml index 9cd5700..d1d1c20 100644 --- a/.github/workflows/build-images.yaml +++ b/.github/workflows/build-images.yaml @@ -43,6 +43,7 @@ jobs: with: image: nebari-data-science-pack-jupyterlab target: jupyterlab + smoke_test_labextensions: true secrets: QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} @@ -67,6 +68,7 @@ jobs: build_arm64: false base_image: "nvidia/cuda:12.8.1-base-ubuntu24.04" gpu: true + smoke_test_labextensions: true secrets: QUAY_TOKEN: ${{ secrets.QUAY_TOKEN }} diff --git a/docs/src/content/docs/nebi-integration.md b/docs/src/content/docs/nebi-integration.md index 24fd65c..9a7316d 100644 --- a/docs/src/content/docs/nebi-integration.md +++ b/docs/src/content/docs/nebi-integration.md @@ -78,6 +78,11 @@ Leaving `tag` empty disables the init container entirely — no Nebi in user pod pinned per chart release (`scripts/bump_image_tags.py` only handles the JupyterLab images), so override it to test a PR build or to roll forward between chart releases. +When overriding, keep the binary **≥ v0.15**: `jupyterlab-launchpad` (≥ 1.1.1, pinned in +the JupyterLab image) listens for `nebi:job-completed` from this binary to refresh the +kernel list immediately after an environment build. Older nebi never sends the message, +so kernels silently fall back to the ~61s `KernelSpecManager` poll with no error logged. + ## The token exchange Nebi needs a per-user credential, and the hub is the only component holding the user's @@ -212,6 +217,7 @@ CA merge step is ordered before it, so the bundle is ready. See | Empty environment dropdown in jhub-apps | Exchange failure, or `auth_state` missing — `kubectl logs deploy/hub \| grep nebi-envs`. | | Registry changes not visible | Hub not restarted since the change. | | Apps missing packages in a Nebi env | `jhub-app-proxy-version` below v0.2.3. | +| New kernels take ~61s to appear after an env build | Checkable: `nebi.image.tag` below v0.15 (no `nebi:job-completed` sender — `kubectl -n data-science exec -- nebi --version`), or launchpad below 1.1.1 / disabled (`kubectl -n data-science exec -- jupyter labextension list 2>&1 \| grep launchpad`). Not checkable — no signal anywhere in this stack: the Nebi tab wasn't open when the build finished, or nebi fell back to team mode after a slow start (`/version` unreachable for ~900ms at handshake time). Re-test with the Nebi tab open. | ```bash # Is the binary in the pod? diff --git a/docs/src/content/docs/server-profiles.md b/docs/src/content/docs/server-profiles.md index 7693945..6c9c2a8 100644 --- a/docs/src/content/docs/server-profiles.md +++ b/docs/src/content/docs/server-profiles.md @@ -81,8 +81,10 @@ image field; the inner `profile_options.image.choices.default` is what the Jupyt profile selector shows. Both must be bumped alongside `jupyterhub.singleuser.image.tag`. z2jh values cannot reference other values, so the duplication is unavoidable. -`scripts/bump_image_tags.py` syncs all three on an automated bump — hand edits have to keep -up on their own. +`scripts/bump_image_tags.py` syncs all three on an automated bump, and +`tests/unit/test_image_ref_sync.py` fails CI if a hand edit lets any jupyterlab-tagged ref +in this repo's `values.yaml` drift from `singleuser.image`. Choices pointing at other +images (like the R image above) are left alone by both. ::: ## Gating profiles by group diff --git a/docs/src/content/docs/values-reference.md b/docs/src/content/docs/values-reference.md index 2b03f3f..64b0d8a 100644 --- a/docs/src/content/docs/values-reference.md +++ b/docs/src/content/docs/values-reference.md @@ -142,7 +142,7 @@ Admin guide: [Nebi integration](/nebi-integration/). | Field | Default | What it does | |---|---|---| | `nebi.image.repository` | `quay.io/nebari/nebi` | Binary copied into user pods by an init container. | -| `nebi.image.tag` | `sha-5ca877a` | Pinned per chart release. Empty disables the init container. | +| `nebi.image.tag` | `sha-5ca877a` | Pinned per chart release. Must be ≥ v0.15 for `jupyterlab-launchpad` (≥ 1.1.1) to receive `nebi:job-completed`; older nebi never sends it and kernel refresh silently falls back to the ~61s poll. Empty disables the init container. | | `nebi.image.pullPolicy` | `IfNotPresent` | — | | `nebi.remoteURL` | `""` *(derived)* | Browser-facing Nebi URL, used for the OIDC redirect. | | `nebi.internalURL` | `""` *(derived)* | In-cluster URL for the token-exchange path. | diff --git a/images/jupyterlab/pixi.lock b/images/jupyterlab/pixi.lock index 6cfbf3f..0f5ff60 100644 --- a/images/jupyterlab/pixi.lock +++ b/images/jupyterlab/pixi.lock @@ -448,6 +448,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8c/bb941f9251290a3f01b75124530fd102bfd51948c30c0cdf7da00644f785/jupyterlab_launchpad-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/f6/73c4aa003d1237ee9bea8a46f49dc38c45dfe95af4f0da7e60678d388011/trove_classifiers-2025.11.14.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/e3/f1fae3647d170919c2cf2a898e77e7d1a4e5c7cae0aed7bb4bd3f5ebff6f/filelock-3.29.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/5b/16e50abf152be7f18120f11dfff495014a9eaff7b764626e1656f04ad262/pynvml-11.5.3-py3-none-any.whl @@ -484,7 +485,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/3e/8228047497767397d5e9ef195fb2739b6ed755d042ad3c0edb28f4f6d578/conda_lock-3.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d8/3a/33189fc5f72f31d334345dfeca7a634c4949ad20e1c5093188f25e2c9c24/jupyterlab_launchpad-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/da/e5/148ab5edb339f5833d04f0bcb8380a53e8b19bd5f091ae67222ed188b393/uv-0.9.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl @@ -927,6 +927,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/45/7f/0e961cf3908bc4c1c3e027de2794f867c6c89fb4916fc7dba295a0e80a2d/boltons-25.0.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/8c/bb941f9251290a3f01b75124530fd102bfd51948c30c0cdf7da00644f785/jupyterlab_launchpad-1.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/49/f6/73c4aa003d1237ee9bea8a46f49dc38c45dfe95af4f0da7e60678d388011/trove_classifiers-2025.11.14.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/e3/f1fae3647d170919c2cf2a898e77e7d1a4e5c7cae0aed7bb4bd3f5ebff6f/filelock-3.29.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/54/5b/16e50abf152be7f18120f11dfff495014a9eaff7b764626e1656f04ad262/pynvml-11.5.3-py3-none-any.whl @@ -963,7 +964,6 @@ environments: - pypi: https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/3e/8228047497767397d5e9ef195fb2739b6ed755d042ad3c0edb28f4f6d578/conda_lock-3.0.4-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/d8/3a/33189fc5f72f31d334345dfeca7a634c4949ad20e1c5093188f25e2c9c24/jupyterlab_launchpad-1.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl @@ -9296,6 +9296,14 @@ packages: version: 25.0.0 sha256: dc9fb38bf28985715497d1b54d00b62ea866eca3938938ea9043e254a3a6ca62 requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/47/8c/bb941f9251290a3f01b75124530fd102bfd51948c30c0cdf7da00644f785/jupyterlab_launchpad-1.1.1-py3-none-any.whl + name: jupyterlab-launchpad + version: 1.1.1 + sha256: 24a7af666d42f552491ae01d00a1d18d5dcd88b547e470a7ad00f5c08d4cc6ba + requires_dist: + - jupyter-server>=2.0.1,<3 + - jupyter-server-proxy ; extra == 'test' + requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/49/f6/73c4aa003d1237ee9bea8a46f49dc38c45dfe95af4f0da7e60678d388011/trove_classifiers-2025.11.14.15-py3-none-any.whl name: trove-classifiers version: 2025.11.14.15 @@ -9791,14 +9799,6 @@ packages: - xattr>=1.0.0,<2.0.0 ; sys_platform == 'darwin' - zstandard>=0.15 requires_python: '>=3.9' -- pypi: https://files.pythonhosted.org/packages/d8/3a/33189fc5f72f31d334345dfeca7a634c4949ad20e1c5093188f25e2c9c24/jupyterlab_launchpad-1.1.0-py3-none-any.whl - name: jupyterlab-launchpad - version: 1.1.0 - sha256: cd8ca17985e961cdb836f3d6335c640724aacebbf6adef5bd163f3f397106e33 - requires_dist: - - jupyter-server>=2.0.1,<3 - - jupyter-server-proxy ; extra == 'test' - requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl name: tqdm version: 4.68.3 diff --git a/images/jupyterlab/pixi.toml b/images/jupyterlab/pixi.toml index 72f5970..18a646b 100644 --- a/images/jupyterlab/pixi.toml +++ b/images/jupyterlab/pixi.toml @@ -78,6 +78,8 @@ jupyterlab_nvdashboard = "==0.12.0" # argo-jupyter-scheduler = "==2024.6.1" # disabled until service is configured jhub-apps = "==2026.8.1" jupyterlab-nebari-mode = "==0.3.0" -jupyterlab-launchpad = "==1.1.0" +# >=1.1.1 listens for nebi:job-completed to refresh kernels; the sender is the +# nebi binary pinned at values.yaml nebi.image.tag, which must be >= v0.15. +jupyterlab-launchpad = "==1.1.1" jupyterlab-gallery = "==0.6.3" jupyterlab-jhub-apps = "==0.3.1" diff --git a/tests/unit/test_image_ref_sync.py b/tests/unit/test_image_ref_sync.py new file mode 100644 index 0000000..490bd06 --- /dev/null +++ b/tests/unit/test_image_ref_sync.py @@ -0,0 +1,112 @@ +"""Structural tests that every hand-editable jupyterlab image reference in +values.yaml agrees with ``jupyterhub.singleuser.image``. + +e2e derives its cache key and kind side-load from ``singleuser.image``, but +the pod that actually spawns comes from the *default profile*: the spawn +POST has no body, so kubespawner falls through to the ``default: true`` +profile, whose ``profile_options`` default choice overwrites the image. +A bump that moves ``singleuser.image.tag`` but misses a profile ref would +therefore have e2e report the new tag while the pod pulls the old image — +green CI on stale code. ``scripts/bump_image_tags.py`` keeps these in sync +on the automated path; these asserts catch the hand-edit path. + +Scope mirrors the script's guards exactly: only refs pointing at the +jupyterlab image (``JUPYTERLAB_IMAGE:`` / ``JUPYTERLAB_DISPLAY_PREFIX:``) +must agree. Additional choices pointing elsewhere (e.g. the R image in +server-profiles.md) and variant profiles that carry no image are +intentionally out of scope, just as the script leaves them alone. +""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + +from scripts.bump_image_tags import JUPYTERLAB_DISPLAY_PREFIX, JUPYTERLAB_IMAGE + +REPO_ROOT = Path(__file__).resolve().parents[2] +VALUES_YAML = REPO_ROOT / "values.yaml" + + +def _jupyterhub_values(): + with VALUES_YAML.open() as f: + return yaml.safe_load(f)["jupyterhub"] + + +def _singleuser_ref(jh): + image = jh["singleuser"]["image"] + return f'{image["name"]}:{image["tag"]}' + + +def test_singleuser_image_is_the_jupyterlab_image(): + """Anchor for the guarded tests below: if singleuser.image were renamed + away from the script's JUPYTERLAB_IMAGE constant, every guard would stop + matching and the other tests would pass vacuously. Fail loudly instead.""" + jh = _jupyterhub_values() + assert jh["singleuser"]["image"]["name"] == JUPYTERLAB_IMAGE, ( + "singleuser.image.name no longer matches bump_image_tags.py's " + "JUPYTERLAB_IMAGE — update the script constant and these tests " + "together, or the sync guards silently stop guarding" + ) + + +def test_profile_images_match_singleuser(): + """Every jupyterlab-tagged image ref in the profiles — the outer + kubespawner_override.image and any profile_options choice pointing at + the jupyterlab image — must equal singleuser.image. The default choice + is what the spawned pod actually runs.""" + jh = _jupyterhub_values() + ref = _singleuser_ref(jh) + profiles = jh["custom"]["profiles"] + assert profiles, "no profiles found under jupyterhub.custom.profiles" + for profile in profiles: + slug = profile["slug"] + outer = profile.get("kubespawner_override", {}).get("image", "") + if outer.startswith(JUPYTERLAB_IMAGE + ":"): + assert outer == ref, ( + f"profile {slug!r}: kubespawner_override.image does not " + f"match singleuser.image ({ref}) — jhub-apps' Create App " + "shows this value; a half-bump here spawns a stale image" + ) + options = profile.get("profile_options", {}).get("image", {}) + for name, choice in options.get("choices", {}).items(): + img = choice.get("kubespawner_override", {}).get("image", "") + if img.startswith(JUPYTERLAB_IMAGE + ":"): + assert img == ref, ( + f"profile {slug!r} choice {name!r}: image does not match " + f"singleuser.image ({ref}) — this choice overwrites the " + "pod image at spawn, so e2e would report the new tag " + "while the pod pulls the old one" + ) + + +def test_profile_choice_display_names_match_image(): + """Every choice whose display_name carries the jupyterlab image name + embeds the tag the profile selector shows; it must name the image + singleuser.image actually spawns.""" + jh = _jupyterhub_values() + tag = jh["singleuser"]["image"]["tag"] + expected = f"{JUPYTERLAB_DISPLAY_PREFIX}:{tag}" + for profile in _jupyterhub_values()["custom"]["profiles"]: + options = profile.get("profile_options", {}).get("image", {}) + for name, choice in options.get("choices", {}).items(): + display = choice.get("display_name", "") + if display.startswith(JUPYTERLAB_DISPLAY_PREFIX + ":"): + assert display == expected, ( + f'profile {profile["slug"]!r} choice {name!r}: ' + f"display_name {display!r} does not match the image it " + f"spawns ({expected}) — the selector would show one tag " + "and run another" + ) + + +def test_hub_tag_matches_singleuser_tag(): + """hub and jupyterlab images are built from the same commit and tagged + with the same sha; a half-bump that moves the hub pair but not the + jupyterlab refs (or vice versa) must not pass unnoticed.""" + jh = _jupyterhub_values() + assert jh["hub"]["image"]["tag"] == jh["singleuser"]["image"]["tag"], ( + "hub.image.tag and singleuser.image.tag are bumped together from the " + "same commit's build; a mismatch means a partial hand-bump" + ) diff --git a/values.yaml b/values.yaml index ce05883..338e254 100644 --- a/values.yaml +++ b/values.yaml @@ -253,6 +253,10 @@ nebi: # handles the jupyterlab images); deployers can override to test a PR # build or roll forward. Leave non-empty so the init container is wired # by default. sha-5ca877a == the nebi v0.15 tag commit. + # Must be >= v0.15: jupyterlab-launchpad >= 1.1.1 (pinned in + # images/jupyterlab/pixi.toml) listens for `nebi:job-completed` from this + # binary. Older nebi sends nothing, so kernel refresh silently falls back + # to the ~61s KernelSpecManager poll — no error is logged anywhere. tag: "sha-5ca877a" pullPolicy: IfNotPresent # External Nebi FQDN (browser-side, used for OIDC redirect). @@ -489,10 +493,13 @@ jupyterhub: # kubespawner_override: # extra_resource_limits: # nvidia.com/gpu: 1 - # NOTE: when bumping singleuser.image.tag below, also bump the - # ``image: ...`` lines inside each profile_options.image.choices.default - # entry so the profile selector shows the right tag. (z2jh values.yaml - # cannot reference other values, so the duplication is unavoidable.) + # NOTE: when bumping singleuser.image.tag below, also bump — in EACH + # profile — the outer ``kubespawner_override.image``, the ``image: ...`` + # line inside profile_options.image.choices.default, and that choice's + # ``display_name`` (it embeds the tag the selector shows). (z2jh + # values.yaml cannot reference other values, so the duplication is + # unavoidable.) tests/unit/test_image_ref_sync.py fails if any of them + # drift from singleuser.image. profiles: - slug: small-instance display_name: "Small Instance" @@ -504,7 +511,7 @@ jupyterhub: # The same value sits inside profile_options.image.choices.default # so the JupyterLab profile selector keeps showing it too. # scripts/bump_image_tags.py syncs all three on every bump. - image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-08ea5e4 + image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-7c0599c cpu_limit: 1 cpu_guarantee: 0.5 mem_limit: "2G" @@ -514,15 +521,15 @@ jupyterhub: display_name: Image choices: default: - display_name: "nebari-data-science-pack-jupyterlab:sha-08ea5e4" + display_name: "nebari-data-science-pack-jupyterlab:sha-7c0599c" default: true kubespawner_override: - image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-08ea5e4 + image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-7c0599c - slug: medium-instance display_name: "Medium Instance" description: "4 CPU / 8 GB RAM — pandas / scikit-learn workloads on medium datasets." kubespawner_override: - image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-08ea5e4 + image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-7c0599c cpu_limit: 4 cpu_guarantee: 2 mem_limit: "8G" @@ -532,10 +539,10 @@ jupyterhub: display_name: Image choices: default: - display_name: "nebari-data-science-pack-jupyterlab:sha-08ea5e4" + display_name: "nebari-data-science-pack-jupyterlab:sha-7c0599c" default: true kubespawner_override: - image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-08ea5e4 + image: quay.io/nebari/nebari-data-science-pack-jupyterlab:sha-7c0599c # Terminal customization: controls Starship prompt in JupyterLab terminals. # When false, falls back to the default bash prompt. terminal-customization: true @@ -597,7 +604,7 @@ jupyterhub: # class clears current_user — id_token_hint now reaches KC). image: name: quay.io/nebari/nebari-data-science-pack-jupyterhub - tag: "sha-08ea5e4" + tag: "sha-7c0599c" config: JupyterHub: @@ -700,7 +707,7 @@ jupyterhub: # keeps the two in sync on every automated bump instead. initContainers: - name: merge-ca-bundle - image: quay.io/nebari/nebari-data-science-pack-jupyterhub:sha-08ea5e4 + image: quay.io/nebari/nebari-data-science-pack-jupyterhub:sha-7c0599c imagePullPolicy: IfNotPresent # z2jh's pod-level securityContext sets ``runAsNonRoot: true`` but # no explicit uid, and the hub image's default ``USER`` is root, @@ -767,7 +774,7 @@ jupyterhub: singleuser: image: name: quay.io/nebari/nebari-data-science-pack-jupyterlab - tag: "sha-08ea5e4" + tag: "sha-7c0599c" defaultUrl: "/lab" extraEnv: JUPYTERHUB_SINGLEUSER_APP: "jupyter_server.serverapp.ServerApp"