From 7d451a7ce912be4100879e9c5e49b426d0a50a42 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 18 Oct 2026 22:39:39 +0200 Subject: [PATCH] feat(ide): add browser VS Code for course notebooks --- README.md | 15 ++- START_HERE.md | 72 +++++++++++--- docker-compose.yml | 42 +++++++- docs/course_prd.md | 6 +- docs/course_program.md | 8 +- docs/maintainer_guide.md | 4 +- docs/stack_reference.md | 79 +++++++++++++-- ide/Dockerfile | 35 +++++++ ide/lakehouse.code-workspace | 10 ++ ide/settings.json | 8 ++ jupyter/Dockerfile | 2 +- minio/Dockerfile | 46 +++++++++ notebooks/01_environment_and_smoke_test.ipynb | 22 ++--- tests/browser_ide.cjs | 98 +++++++++++++++++++ tests/browser_ide.sh | 33 +++++++ 15 files changed, 430 insertions(+), 50 deletions(-) create mode 100644 ide/Dockerfile create mode 100644 ide/lakehouse.code-workspace create mode 100644 ide/settings.json create mode 100644 minio/Dockerfile create mode 100644 tests/browser_ide.cjs create mode 100644 tests/browser_ide.sh diff --git a/README.md b/README.md index 166b9c9..025ab45 100755 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ graph TB U[Student / engineer] subgraph Access - J[JupyterLab
notebooks / PySpark] + J[VS Code в браузере
Jupyter notebooks / PySpark] Q[Trino UI / CLI
ad hoc SQL] end @@ -54,7 +54,8 @@ graph TB | `PostgreSQL` | Catalog — метаданные JDBC-каталога `lakehouse` | | `Spark` | Compute — ETL, запись и чтение Iceberg-таблиц | | `Trino` | Compute — ad hoc SQL, чтение тех же таблиц | -| `JupyterLab` | Точка входа — практические ноутбуки курса | +| `code-server` | VS Code в браузере — ноутбуки курса, Python и терминал внутри Docker | +| `JupyterLab` | Запасной интерфейс для тех же ноутбуков | Ноутбуки подключаются к Spark-кластеру с [готовыми настройками стенда](./docs/stack_reference.md#spark). @@ -73,18 +74,22 @@ docker compose up -d docker compose ps ``` -Зафиксированные исторические образы MinIO загружаются из официального Quay registry; подробности — в [техническом reference](./docs/stack_reference.md#minio). +MinIO собирается из закреплённых официальных исходников; первая сборка занимает +несколько дополнительных минут. Подробности — в [техническом reference](./docs/stack_reference.md#minio). После старта: | UI | Адрес | | --- | --- | -| JupyterLab | `http://localhost:8888` | +| VS Code в браузере | `http://localhost:8889` | +| JupyterLab (запасной интерфейс) | `http://localhost:8888` | | Spark Master | `http://localhost:8080` | | Trino | `http://localhost:8090` | | MinIO Console | `http://localhost:9001` | -JupyterLab опубликован только на loopback-интерфейсе хоста и рассчитан на локальный одно-пользовательский стенд. Подключение через VS Code Remote-SSH описано в [START_HERE.md](./START_HERE.md#как-работать-в-jupyter). +Открой ноутбук в браузерном VS Code и выбери Python внутри контейнера: локальный +Python и venv не нужны. Оба интерфейса доступны только с хоста стенда. +Первый запуск ноутбука описан в [START_HERE.md](./START_HERE.md#как-работать-с-ноутбуками). ## Документы для прохождения diff --git a/START_HERE.md b/START_HERE.md index f4ad300..efb3906 100644 --- a/START_HERE.md +++ b/START_HERE.md @@ -8,7 +8,7 @@ 2. Собрать и поднять стенд. 3. Убедиться, что контейнеры живы. 4. Открыть основные UI. -5. Зайти в Jupyter и выполнить `notebooks/01_environment_and_smoke_test.ipynb`. +5. Открыть VS Code в браузере и выполнить `notebooks/01_environment_and_smoke_test.ipynb`. 6. Перед Модулем 3 подготовить учебный датасет в `./data/nyc_taxi`. 7. При проблемах использовать логи и шаги диагностики из этого документа. @@ -17,15 +17,18 @@ Нужно заранее установить: - Docker; -- Docker Compose; +- Docker Compose 2.17+ с BuildKit (`docker compose version`); - современный браузер для UI; - свободные порты на хосте. Рекомендуемые ресурсы хоста: - не менее `4 vCPU`; -- не менее `10 GB RAM`, иначе `Spark`, `Trino` и `Jupyter` могут стартовать нестабильно; -- хотя бы `8-10 GB` свободного места под образы и контейнеры. +- не менее `10 GB RAM`, иначе `Spark`, `Trino` и IDE могут стартовать нестабильно; +- хотя бы `20 GB` свободного места под образы, кэш первой сборки и контейнеры. + +Python, Java, Jupyter и расширения VS Code устанавливаются при сборке Docker-образов. +На хосте не нужно создавать venv или устанавливать Python-пакеты. ## Порты стенда @@ -37,20 +40,31 @@ | Trino UI | `http://localhost:8090` | Проверка координатора Trino | | MinIO API | `http://localhost:9000` | S3-compatible endpoint | | MinIO Console | `http://localhost:9001` | Просмотр бакетов и файлов | -| JupyterLab | `http://localhost:8888` | Основная точка входа в практику | +| VS Code в браузере | `http://localhost:8889` | Основная точка входа в практику | +| JupyterLab | `http://localhost:8888` | Запасной интерфейс для ноутбуков | | PostgreSQL | `localhost:5432` | JDBC-каталог Iceberg, нужен для диагностики | ## Быстрый запуск Все команды выполняются из корня репозитория. +На Linux пользователь контейнера должен иметь доступ к файлам ноутбуков. Если +твой UID отличается от `1000`, перед сборкой задай его в текущем терминале: + +```bash +export NB_UID=$(id -u) NB_GID=$(id -g) +``` + +Для Docker Desktop на macOS и Windows обычно подходят значения по умолчанию. + ### 1. Собрать образы ```bash docker compose build ``` -Зафиксированные образы MinIO загружаются из `quay.io`; их версии и назначение описаны в [техническом reference](./docs/stack_reference.md#minio). +Первая сборка скачивает зависимости и компилирует MinIO из закреплённых исходников. +На это может уйти несколько минут. Версии описаны в [техническом reference](./docs/stack_reference.md#minio). ### 2. Поднять стенд @@ -66,7 +80,7 @@ docker compose ps Ожидаемое состояние: -- сервисы `spark-master`, `spark-worker-1`, `spark-worker-2`, `minio`, `postgres-iceberg`, `trino`, `jupyter` находятся в состоянии `Up`; +- контейнеры `spark-master`, `spark-worker-1`, `spark-worker-2`, `minio`, `postgres-iceberg`, `trino`, `jupyter`, `ide` находятся в состоянии `Up`, у `ide` появляется статус `healthy`; - однократные init-контейнеры вроде `minio-init` и `iceberg-catalog-init` могут завершиться после успешной инициализации. ## Куда заходить после старта @@ -76,14 +90,14 @@ docker compose ps - `http://localhost:8080` для `Spark Master UI`; - `http://localhost:8090` для `Trino UI`; - `http://localhost:9001` для `MinIO Console`; -- `http://localhost:8888` для `JupyterLab`. +- `http://localhost:8889` для `VS Code в браузере`. Для входа в `MinIO Console` используй: - логин: `minioadmin`; - пароль: `minioadmin`. -Если интерфейсы открываются, переходи в Jupyter и запускай: +Если интерфейсы открываются, переходи в VS Code и запускай: ```text notebooks/01_environment_and_smoke_test.ipynb @@ -97,19 +111,38 @@ notebooks/01_environment_and_smoke_test.ipynb | `PostgreSQL` | `catalog`: хранит метаданные JDBC-каталога Iceberg | | `Spark` | `compute`: выполняет PySpark и Spark SQL задания | | `Trino` | `compute`: читает те же таблицы через общий каталог | -| `Jupyter` | Точка входа в учебные ноутбуки | +| `VS Code / JupyterLab` | Интерфейсы для учебных ноутбуков | -## Как работать в Jupyter +## Как работать с ноутбуками + +1. Открой `http://localhost:8889` в отдельной вкладке браузера. +2. Если сверху виден **Restricted Mode**, нажми **Manage → Trust** и закрой окно + Workspace Trust. Подтверди доверие только к своей копии этого учебного + репозитория: в Restricted Mode запуск ноутбуков ограничен. +3. В папке **Ноутбуки** открой `01_environment_and_smoke_test.ipynb`. +4. Нажми **Select Kernel → Python Environments** и выбери Python с путём + `/usr/bin/python3`. Он находится внутри контейнера `ide`, где уже есть + зависимости курса. Пункт **Existing Jupyter Server** здесь не нужен. +5. Запусти первую кодовую ячейку кнопкой слева или `Shift+Enter`, затем остальные + ячейки по порядку. Результат появится под ячейкой. Сохрани ноутбук через `Ctrl+S`. + +Терминал VS Code тоже работает внутри контейнера. Команды `docker compose` +выполняй в терминале хоста, откуда запускал стенд. + +Файлы доступны в IDE так: -- JupyterLab доступен без логина только с хоста, на котором запущен стенд; -- при работе через VS Code Remote-SSH выбери `Existing Jupyter Server` и укажи `http://127.0.0.1:8888/`; -- token в адрес добавлять не нужно: аутентификация и XSRF-проверка отключены для локального одно-пользовательского сценария; - `./notebooks` смонтирован в контейнер как `/opt/work`; - `./src` смонтирован read-only как `/opt/src`; +- весь репозиторий доступен read-only в `/opt/course`; его папка `notebooks/` + доступна для записи, поэтому правки из IDE сохраняются на хосте; - `./data` смонтирован read-only как `/opt/data`; - `PYTHONPATH=/opt/src`, поэтому helper-скрипты из `src/` доступны для импорта в ноутбуках; -- первый ноутбук курса: `01_environment_and_smoke_test.ipynb`; -- локальный учебный data bundle для Модуля 3 должен быть доступен внутри Jupyter по пути `/opt/data/nyc_taxi`. +- учебный data bundle для Модуля 3 доступен по пути `/opt/data/nyc_taxi`. + +Запасной интерфейс — JupyterLab на `http://localhost:8888`. Файлы ноутбуков общие, +а ядра и переменные Python отдельные. Перед переходом сохрани ноутбук и останови +его ядро. Подключение с удалённой машины описано в +[техническом reference](./docs/stack_reference.md#vs-code-в-браузере). Ноутбуки берут адрес кластера из [`spark/spark-defaults.conf`](./spark/spark-defaults.conf). Если изменил его, @@ -132,6 +165,7 @@ docker compose logs -f spark-master docker compose logs -f trino docker compose logs -f minio docker compose logs -f jupyter +docker compose logs -f ide ``` ### Типовые первые проверки @@ -139,6 +173,12 @@ docker compose logs -f jupyter - `Spark UI` не открывается: проверь `docker compose ps` и логи `spark-master`. - `Trino UI` не открывается: проверь `docker compose ps` и логи `trino`. - `JupyterLab` не открывается: проверь `docker compose ps` и логи `jupyter`. +- VS Code не открывается: проверь `docker compose ps ide` и логи `ide`. +- Вместо ноутбука ошибка webview/service worker: открой `http://localhost:8889` + в отдельной вкладке, без IP сервера и без встраивания в другой сайт. +- Ядро не выбирается: убедись, что рабочая область доверенная, и выбери + `/usr/bin/python3` в **Python Environments**. Для подробностей открой + **View → Output → Jupyter**. - `MinIO Console` не открывается: проверь `docker compose ps` и логи `minio`. - smoke test падает из ноутбука: сначала убедись, что `spark-master` и worker-ы видны в `Spark UI`. diff --git a/docker-compose.yml b/docker-compose.yml index 73bf191..278504c 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -121,7 +121,9 @@ services: - ./spark/spark-defaults.conf:/opt/spark/conf/spark-defaults.conf:ro minio: - image: quay.io/minio/minio:RELEASE.2024-10-02T17-50-41Z + build: + context: ./minio + target: minio container_name: minio hostname: minio networks: @@ -137,7 +139,9 @@ services: - minio-data:/data minio-init: - image: quay.io/minio/mc:RELEASE.2024-06-24T19-40-33Z-cpuv1 + build: + context: ./minio + target: mc container_name: minio-init hostname: minio-init networks: @@ -160,6 +164,8 @@ services: jupyter: build: context: ./jupyter + additional_contexts: + spark-runtime: service:spark-master args: NB_UID: ${NB_UID:-1000} NB_GID: ${NB_GID:-100} @@ -192,6 +198,38 @@ services: - ./src:/opt/src:ro - ./data:/opt/data:ro + ide: + build: + context: ./ide + additional_contexts: + jupyter-runtime: service:jupyter + container_name: ide + hostname: ide + depends_on: + - spark-master + - minio + - iceberg-catalog-init + networks: + - spark-net + environment: + - AWS_ACCESS_KEY_ID=minioadmin + - AWS_SECRET_ACCESS_KEY=minioadmin + - PYTHONPATH=/opt/src + ports: + - "127.0.0.1:8889:8889" + volumes: + - ./spark/spark-defaults.conf:/opt/spark/conf/spark-defaults.conf:ro + - ./notebooks:/opt/work + - ./src:/opt/src:ro + - ./data:/opt/data:ro + - .:/opt/course:ro + - ./notebooks:/opt/course/notebooks + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8889/healthz"] + interval: 10s + timeout: 5s + retries: 6 + trino: image: trinodb/trino:478 container_name: trino diff --git a/docs/course_prd.md b/docs/course_prd.md index 716c2e0..c9a0024 100644 --- a/docs/course_prd.md +++ b/docs/course_prd.md @@ -96,7 +96,7 @@ * snapshot/time travel на прикладном уровне; * базовый `compaction/vacuum` с параллелями к `Greenplum AppendOnly`; * набор практик в Jupyter Notebooks с обязательными заданиями и checkpoints; -* стартовая документация до первого ноутбука: prerequisites, запуск стенда, вход в Jupyter и базовый troubleshooting. +* стартовая документация до первого ноутбука: prerequisites, запуск стенда, вход в браузерный VS Code и базовый troubleshooting. Ориентировочная программа курса вынесена в отдельный документ: **[course_program.md](./course_program.md)**. @@ -152,7 +152,7 @@ **Acceptance Criteria для учебных материалов и результата:** -1. В составе курса есть стартовая документация до первого ноутбука, включая prerequisites, запуск стенда, вход в `Jupyter` и получение учебного data bundle. +1. В составе курса есть стартовая документация до первого ноутбука, включая prerequisites, запуск стенда, вход в браузерный VS Code и получение учебного data bundle. 2. Студент проходит обязательные `checkpoints` по урокам. 3. Студент самостоятельно выполняет набор практик на одном датасете, включая: * подключение к Spark и загрузку raw-данных; @@ -174,7 +174,7 @@ **В скоупе:** -* использование существующего `docker-compose.yml` (`Spark`, `Trino`, `MinIO`, `PostgreSQL`, `Jupyter`); +* использование существующего `docker-compose.yml` (`Spark`, `Trino`, `MinIO`, `PostgreSQL`, `code-server`, `JupyterLab`); * подготовка учебных датасетов и/или инструкций по их загрузке в репозиторий; * наличие воспроизводимого способа получить учебный data bundle без ручного поиска по внешним сайтам; * поддержка выполнения запросов к `Trino` из ноутбуков; diff --git a/docs/course_program.md b/docs/course_program.md index 9ce667f..74602b4 100644 --- a/docs/course_program.md +++ b/docs/course_program.md @@ -27,7 +27,7 @@ ## 3. Состав учебных материалов -Курс не должен состоять только из ноутбуков. Ноутбуки являются ядром практики, но вход в курс, запуск стенда и правила работы должны быть вынесены в обычную документацию, доступную до старта `Jupyter`. +Курс не должен состоять только из ноутбуков. Ноутбуки являются ядром практики, но вход в курс, запуск стенда и правила работы должны быть вынесены в обычную документацию, доступную до открытия IDE. ### 3.1. Стартовая документация до первого ноутбука @@ -38,7 +38,7 @@ * `START_HERE.md` или аналогичный стартовый документ с маршрутом прохождения курса; * инструкция по prerequisites: `Docker`, `Docker Compose`, свободные порты, базовые команды; * пошаговый гайд по запуску стенда: `build`, `up`, проверка сервисов, открытие UI; -* краткое объяснение, как пользоваться `Jupyter`, где лежат ноутбуки и как читать структуру урока; +* краткое объяснение, как запускать Jupyter-ноутбуки в браузерном VS Code, где лежат ноутбуки и как читать структуру урока; * troubleshooting по типовым проблемам старта; * инструкция по reset/restart стенда для повторного прохождения практик. @@ -46,7 +46,7 @@ * студент понимает, что нужно установить и проверить до начала курса; * студент может поднять стенд без ментора; -* студент знает, как попасть в `Jupyter`, `MinIO`, `Trino UI`; +* студент знает, как попасть в браузерный VS Code, `MinIO`, `Trino UI`; * студент понимает, как устроен формат уроков и что от него ожидается. ### 3.2. Практическое ядро курса @@ -88,7 +88,7 @@ * запуск `docker compose build` и `docker compose up -d`; * проверка сервисов и веб-интерфейсов; * базовый smoke test для Spark; -* первый обзор того, где в стенде `MinIO`, `PostgreSQL`, `Spark`, `Trino`, `Jupyter`. +* первый обзор того, где в стенде `MinIO`, `PostgreSQL`, `Spark`, `Trino` и IDE. **Практика студента:** diff --git a/docs/maintainer_guide.md b/docs/maintainer_guide.md index c0deab3..5d17951 100644 --- a/docs/maintainer_guide.md +++ b/docs/maintainer_guide.md @@ -17,7 +17,7 @@ | Зона | Где лежит | Назначение | | --- | --- | --- | -| Runtime / stand | `docker-compose.yml`, `spark/`, `trino/`, `jupyter/` | Топология сервисов, образы, конфиги, порты | +| Runtime / stand | `docker-compose.yml`, `spark/`, `trino/`, `jupyter/`, `ide/`, `minio/` | Топология сервисов, образы, конфиги, порты | | Onboarding | `README.md`, `START_HERE.md` | Вход в репозиторий и первый пользовательский маршрут | | Runtime reference | `docs/stack_reference.md` | Технические детали стенда: порты, доступ, команды, smoke-тесты | | Course definition | `docs/course_prd.md`, `docs/course_program.md` | Границы курса, learning outcomes, структура модулей | @@ -31,7 +31,7 @@ | Файл или каталог | За что отвечает | Что не стоит туда складывать | | --- | --- | --- | | `docker-compose.yml` | Реальный состав сервисов, контейнеров, сетей, портов и зависимостей | Учебные пояснения, которые не нужны для запуска | -| `spark/`, `trino/`, `jupyter/` | Конкретные runtime-конфиги и образы | Описание программы курса | +| `spark/`, `trino/`, `jupyter/`, `ide/`, `minio/` | Конкретные runtime-конфиги и образы | Описание программы курса | | `README.md` | Верхнеуровневое объяснение репозитория, архитектуры и точек входа | Пошаговый student onboarding во всех деталях и low-level runtime reference | | `START_HERE.md` | Первый маршрут студента: prerequisites, запуск, первые UI, первый ноутбук, базовая диагностика | Полный PRD курса или подробный бэклог модулей | | `docs/stack_reference.md` | Технический reference стенда: сервисы, порты, доступ, reset, smoke-тесты | Роль основного onboarding-документа или описание всей программы курса | diff --git a/docs/stack_reference.md b/docs/stack_reference.md index 3537a06..55ded82 100644 --- a/docs/stack_reference.md +++ b/docs/stack_reference.md @@ -15,7 +15,8 @@ | MinIO API | `minio` | `9000` | S3-compatible endpoint | | MinIO Console | `minio` | `9001` | веб-консоль бакетов и объектов | | PostgreSQL | `postgres-iceberg` | `5432` | JDBC-каталог Iceberg | -| JupyterLab | `jupyter` | `8888` | практические ноутбуки | +| VS Code в браузере (code-server) | `ide` | `8889` | основной интерфейс для ноутбуков и терминала | +| JupyterLab | `jupyter` | `8888` | запасной интерфейс для тех же ноутбуков | ## Основные команды @@ -36,6 +37,7 @@ docker compose logs -f spark-master docker compose logs -f trino docker compose logs -f minio docker compose logs -f jupyter +docker compose logs -f ide ``` ### Остановка и reset @@ -72,8 +74,7 @@ docker compose down -v Пример запуска smoke-скрипта: ```bash -docker compose exec spark-master \ - /opt/spark/bin/spark-submit /opt/src/spark/cluster_smoke.py +docker compose exec ide python3 /opt/src/spark/cluster_smoke.py ``` ### Trino @@ -106,7 +107,13 @@ SHOW TABLES FROM lakehouse.default; - логин: `minioadmin` - пароль: `minioadmin` -Стенд использует зафиксированные исторические образы MinIO Server и `mc` из официального namespace `quay.io/minio`. Эти версии подходят для локального курса, но больше не получают обновлений upstream и не предназначены для production. +Стенд собирает MinIO Server `RELEASE.2024-10-02T17-50-41Z` и `mc` +`RELEASE.2024-06-24T19-40-33Z` из официальных исходников. Коммиты и SHA-256 архивов +зафиксированы в `minio/Dockerfile`. Готовые образы этих версий в Docker Hub и Quay +больше не загружаются; бинарный архив `dl.min.io` также недоступен. +Первая сборка компилирует Go-код и занимает несколько дополнительных минут. +Версии сохранены ради совместимости с курсом и его MinIO Console; они больше +не получают обновлений upstream и не предназначены для production. Бакет `lakehouse` обычно создаётся автоматически контейнером `minio-init`. @@ -126,6 +133,44 @@ psql -h localhost -p 5432 -U iceberg -d iceberg docker compose run --rm iceberg-catalog-init ``` +### VS Code в браузере + +Открой `http://localhost:8889`. Сервис `ide` запускает code-server с установленными +расширениями Python и Jupyter. Python, ipykernel, Java, PySpark и зависимости курса +уже находятся в образе. Локальный Python или venv не нужен. + +В рабочей области доступны ноутбуки, примеры из `src/` и документация. Репозиторий +смонтирован в `/opt/course` только для чтения, а его каталог `notebooks/` — для +чтения и записи. Так относительные ссылки между документами и ноутбуками работают +и внутри IDE. Пути `/opt/work`, `/opt/src`, `/opt/data` также доступны примерам курса. +Открой +`01_environment_and_smoke_test.ipynb`, нажми **Select Kernel → Python Environments** +и выбери `/usr/bin/python3`. Это Python внутри контейнера IDE. Расширение Jupyter +запускает ядро там же; адрес отдельного Jupyter Server вводить не требуется. + +Порт IDE опубликован только на `127.0.0.1`, без пароля, для одного пользователя. +Открывай именно `localhost` или `127.0.0.1`: браузер считает их доверенным +контекстом даже по HTTP. Notebook webview использует service worker; при обращении +по обычному HTTP к IP сервера или имени в локальной сети этот механизм может +быть недоступен. Это ограничение secure context, отдельное от CORS и XSRF. +См. [secure contexts](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts) +и [webviews в code-server](https://coder.com/docs/code-server/FAQ#why-do-web-views-not-work). + +Для стенда на удалённой машине используй SSH-туннель: + +```bash +ssh -N -L 8889:127.0.0.1:8889 user@host +``` + +Затем открой `http://localhost:8889` на своём компьютере. Прямой доступ по +публичному адресу не входит в эту конфигурацию. + +Образы собираются в порядке `spark-master → jupyter → ide` через Compose +`additional_contexts` (нужен Docker Compose 2.17+ и BuildKit). Версии code-server +и расширений зафиксированы в `ide/Dockerfile`; расширения скачиваются при сборке. +Автоматическое обновление расширений отключено, чтобы повторный запуск использовал +проверенные версии. + ### JupyterLab - адрес: `http://localhost:8888` @@ -137,6 +182,10 @@ docker compose run --rm iceberg-catalog-init - `./data` смонтирован read-only как `/opt/data` - `PYTHONPATH=/opt/src` +JupyterLab остаётся запасным интерфейсом. Он редактирует те же файлы, но запускает +отдельные ядра: переменные Python между IDE и JupyterLab не общие. Перед сменой +интерфейса сохрани ноутбук и останови его ядро. + Первый ноутбук курса: `notebooks/01_environment_and_smoke_test.ipynb`. Для Модуля 3 локально скачанный data bundle должен лежать на хосте в `./data/nyc_taxi` и будет доступен внутри Jupyter по пути `/opt/data/nyc_taxi`. @@ -159,14 +208,32 @@ docker compose run --rm iceberg-catalog-init | `trino/catalog/lakehouse.properties` | каталог Trino `lakehouse`, JDBC и S3-доступ | | `spark/Dockerfile` | образ Spark с Iceberg, S3A и Python-зависимостями | | `jupyter/Dockerfile` | образ JupyterLab на базе Spark | +| `ide/Dockerfile` | code-server и расширения на базе образа Jupyter | +| `ide/lakehouse.code-workspace` | папки рабочей области и настройки Python | +| `minio/Dockerfile` | сборка закреплённых исходников MinIO Server и `mc` | ## Smoke-тесты +### Браузерная IDE и Jupyter-ноутбук + +На Linux с Docker Engine, после запуска стенда: + +```bash +bash tests/browser_ide.sh +``` + +Проверка запускает Chromium в отдельном контейнере и проходит путь студента: +доверие рабочей области → открытие копии Модуля 1 → выбор Python → Run All → +вывод Spark в webview → сохранение → повторное открытие. Затем проверяет, что +все кодовые ячейки выполнились без ошибок и результат сохранился. Исходный +ноутбук не меняется. Скриншот остаётся в каталоге `/tmp/lakehouse-browser-check.*`, +путь печатается перед запуском. Docker Desktop этим скриптом не проверяется: +он использует Linux host networking. + ### 1. Проверка, что Spark-кластер жив ```bash -docker compose exec spark-master \ - /opt/spark/bin/spark-submit /opt/src/spark/cluster_smoke.py +docker compose exec ide python3 /opt/src/spark/cluster_smoke.py ``` ### 2. Проверка Spark -> Trino через общий каталог diff --git a/ide/Dockerfile b/ide/Dockerfile new file mode 100644 index 0000000..f4c2ca6 --- /dev/null +++ b/ide/Dockerfile @@ -0,0 +1,35 @@ +# Compose передаёт результат сборки jupyter через additional_contexts. +FROM jupyter-runtime + +USER root +ARG CODE_SERVER_VERSION=4.141.0 +ARG PYTHON_EXTENSION_VERSION=2026.4.0 +ARG JUPYTER_EXTENSION_VERSION=2025.9.1 + +# Контрольные суммы .deb из выпуска code-server v4.141.0. +RUN set -eux; \ + arch="$(dpkg --print-architecture)"; \ + case "$arch" in \ + amd64) checksum=f384beb4f3ba207e0bca27d0dbd36dd825d5aa909d78c6d4baa8703eaf6b77ba ;; \ + arm64) checksum=0bca33a9b01180ad88f0b8969a8e34ae1c76417df471697e7cd65c20ef5dd60d ;; \ + *) echo "Unsupported architecture: $arch" >&2; exit 1 ;; \ + esac; \ + curl -fL --retry 3 \ + "https://github.com/coder/code-server/releases/download/v${CODE_SERVER_VERSION}/code-server_${CODE_SERVER_VERSION}_${arch}.deb" \ + -o /tmp/code-server.deb; \ + echo "$checksum /tmp/code-server.deb" | sha256sum -c -; \ + apt-get update; \ + apt-get install -y --no-install-recommends /tmp/code-server.deb; \ + rm -rf /tmp/code-server.deb /var/lib/apt/lists/* + +USER jovyan +ENV HOME=/home/jovyan +RUN code-server --install-extension "ms-python.python@${PYTHON_EXTENSION_VERSION}" \ + && code-server --install-extension "ms-toolsai.jupyter@${JUPYTER_EXTENSION_VERSION}" + +COPY lakehouse.code-workspace /opt/lakehouse.code-workspace +COPY --chown=jovyan settings.json /home/jovyan/.local/share/code-server/User/settings.json +EXPOSE 8889 +CMD ["code-server", "--bind-addr", "0.0.0.0:8889", "--auth", "none", \ + "--disable-telemetry", "--disable-update-check", \ + "/opt/lakehouse.code-workspace"] diff --git a/ide/lakehouse.code-workspace b/ide/lakehouse.code-workspace new file mode 100644 index 0000000..4dcfda1 --- /dev/null +++ b/ide/lakehouse.code-workspace @@ -0,0 +1,10 @@ +{ + "folders": [ + { "name": "Ноутбуки", "path": "/opt/course/notebooks" }, + { "name": "Примеры (только чтение)", "path": "/opt/course/src" }, + { "name": "Документация (только чтение)", "path": "/opt/course/docs" } + ], + "settings": { + "python.defaultInterpreterPath": "/usr/bin/python3" + } +} diff --git a/ide/settings.json b/ide/settings.json new file mode 100644 index 0000000..dc87556 --- /dev/null +++ b/ide/settings.json @@ -0,0 +1,8 @@ +{ + "extensions.autoUpdate": false, + "extensions.autoCheckUpdates": false, + "telemetry.telemetryLevel": "off", + "workbench.startupEditor": "none", + "chat.disableAIFeatures": true, + "workbench.secondarySideBar.defaultVisibility": "hidden" +} diff --git a/jupyter/Dockerfile b/jupyter/Dockerfile index db381b9..8e6c18a 100755 --- a/jupyter/Dockerfile +++ b/jupyter/Dockerfile @@ -1,5 +1,5 @@ # Берем за основу контейнер, приготовленный для spark в spark/Dockerfile -FROM de-spark-iceberg:3.5.1 +FROM spark-runtime USER root ENV DEBIAN_FRONTEND=noninteractive diff --git a/minio/Dockerfile b/minio/Dockerfile new file mode 100644 index 0000000..3f09863 --- /dev/null +++ b/minio/Dockerfile @@ -0,0 +1,46 @@ +# Сохраняем версии стенда после удаления исторических бинарных образов upstream. +FROM golang:1.22.8-bookworm AS build-base +ENV CGO_ENABLED=0 GOMAXPROCS=2 +WORKDIR /src + +FROM build-base AS build-minio +# RELEASE.2024-10-02T17-50-41Z +RUN curl -fL --retry 3 \ + https://codeload.github.com/minio/minio/tar.gz/ded0b19d97bb9f73f4d78382eb131ff80003e272 \ + -o /tmp/source.tar.gz \ + && echo '1619e07c52b91611f399f5415cdf5aa96d3beac418bb909db9d7b6f86bbcfb6f /tmp/source.tar.gz' | sha256sum -c - \ + && tar -xzf /tmp/source.tar.gz --strip-components=1 \ + && rm /tmp/source.tar.gz +RUN go build -p 2 -trimpath -ldflags="-s -w \ + -X github.com/minio/minio/cmd.Version=2024-10-02T17:50:41Z \ + -X github.com/minio/minio/cmd.ReleaseTag=RELEASE.2024-10-02T17-50-41Z \ + -X github.com/minio/minio/cmd.CommitID=ded0b19d97bb9f73f4d78382eb131ff80003e272" \ + -o /out/minio . + +FROM build-base AS build-mc +# RELEASE.2024-06-24T19-40-33Z +RUN curl -fL --retry 3 \ + https://codeload.github.com/minio/mc/tar.gz/3548007d5bd1ba8e2ed98f35d95c4776dd9a7ff9 \ + -o /tmp/source.tar.gz \ + && echo '760ac5bf7eb2e44a67a7dd1e5508853e54c10ca75f2b2f2385c5c227042436c5 /tmp/source.tar.gz' | sha256sum -c - \ + && tar -xzf /tmp/source.tar.gz --strip-components=1 \ + && rm /tmp/source.tar.gz +RUN go build -p 2 -trimpath -ldflags="-s -w \ + -X github.com/minio/mc/cmd.Version=2024-06-24T19:40:33Z \ + -X github.com/minio/mc/cmd.ReleaseTag=RELEASE.2024-06-24T19-40-33Z \ + -X github.com/minio/mc/cmd.CommitID=3548007d5bd1ba8e2ed98f35d95c4776dd9a7ff9" \ + -o /out/mc . + +FROM debian:bookworm-slim AS runtime +COPY --from=build-base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +FROM runtime AS minio +COPY --from=build-minio /out/minio /usr/local/bin/minio +COPY --from=build-minio /src/LICENSE /usr/share/doc/minio/LICENSE +EXPOSE 9000 9001 +ENTRYPOINT ["minio"] + +FROM runtime AS mc +COPY --from=build-mc /out/mc /usr/local/bin/mc +COPY --from=build-mc /src/LICENSE /usr/share/doc/mc/LICENSE +ENTRYPOINT ["mc"] diff --git a/notebooks/01_environment_and_smoke_test.ipynb b/notebooks/01_environment_and_smoke_test.ipynb index 024d0fd..bfafc9d 100644 --- a/notebooks/01_environment_and_smoke_test.ipynb +++ b/notebooks/01_environment_and_smoke_test.ipynb @@ -12,7 +12,7 @@ "На выходе ты должен:\n", "\n", "- понимать, что стенд уже поднят на хосте;\n", - "- подключиться к Spark-кластеру из Jupyter;\n", + "- подключиться к Spark-кластеру из ноутбука;\n", "- выполнить базовый smoke test;\n", "- сопоставить сервисы стенда с их ролями;\n", "- знать первый маршрут диагностики через `docker compose ps`, логи и UI.\n" @@ -27,13 +27,13 @@ "\n", "Перед запуском этого ноутбука пройди шаги из `START_HERE.md`.\n", "\n", - "Важно: `docker compose build`, `docker compose up -d`, `docker compose ps` и просмотр логов выполняются на хосте, а не внутри Jupyter.\n", + "Важно: `docker compose build`, `docker compose up -d`, `docker compose ps` и просмотр логов выполняются в терминале хоста; терминал браузерного VS Code работает внутри контейнера.\n", "\n", "Ожидаемое состояние перед началом:\n", "\n", - "- контейнеры `spark-master`, `spark-worker-1`, `spark-worker-2`, `minio`, `postgres-iceberg`, `trino`, `jupyter` уже подняты;\n", - "- открываются `Spark UI`, `Trino UI`, `MinIO Console` и `JupyterLab`;\n", - "- ты работаешь в Jupyter внутри контейнера `jupyter`.\n" + "- контейнеры `spark-master`, `spark-worker-1`, `spark-worker-2`, `minio`, `postgres-iceberg`, `trino`, `jupyter`, `ide` уже подняты;\n", + "- открываются `Spark UI`, `Trino UI`, `MinIO Console` и браузерный VS Code;\n", + "- ты выбрал ядро Python `/usr/bin/python3` внутри контейнера `ide` (Select Kernel → Python Environments).\n" ] }, { @@ -49,7 +49,7 @@ "| `PostgreSQL` | `catalog`: хранит метаданные JDBC-каталога Iceberg |\n", "| `Spark` | `compute`: выполняет PySpark и Spark SQL |\n", "| `Trino` | `compute`: читает те же таблицы через общий каталог |\n", - "| `Jupyter` | Точка входа в учебные ноутбуки |\n", + "| `VS Code / JupyterLab` | Точка входа в учебные ноутбуки |\n", "\n", "В этом модуле мы не строим пайплайн и не создаём Iceberg-таблицы. Задача только одна: убедиться, что среда рабочая и понятная.\n" ] @@ -120,7 +120,7 @@ "\n", "Если тест проходит, это означает минимум следующее:\n", "\n", - "- Jupyter может создать SparkSession;\n", + "- ядро ноутбука может создать SparkSession;\n", "- Spark подключён к кластерному master;\n", "- задание действительно выполняется, а не падает на первом действии.\n" ] @@ -146,7 +146,7 @@ "\n", "Сделай руками и проверь себя:\n", "\n", - "1. Открой `Spark UI`, `Trino UI`, `MinIO Console` и `JupyterLab`.\n", + "1. Открой `Spark UI`, `Trino UI`, `MinIO Console` и браузерный VS Code.\n", "2. Сопоставь каждый сервис с одной из ролей: `storage`, `catalog`, `compute`, `entrypoint`.\n", "3. Объясни, почему этот smoke test не должен работать в `local[*]` режиме.\n", "4. На хосте выполни `docker compose ps` и посмотри, какие контейнеры должны быть в состоянии `Up`.\n", @@ -166,7 +166,7 @@ " \"PostgreSQL\": \"TODO\",\n", " \"Spark\": \"TODO\",\n", " \"Trino\": \"TODO\",\n", - " \"Jupyter\": \"TODO\",\n", + " \"IDE\": \"TODO\",\n", "}\n", "service_roles\n" ] @@ -181,9 +181,9 @@ "К концу модуля ты должен уметь подтвердить:\n", "\n", "- стенд поднят и основные контейнеры живы;\n", - "- `Spark UI`, `MinIO Console`, `Trino UI` и `JupyterLab` открываются;\n", + "- `Spark UI`, `MinIO Console`, `Trino UI` и браузерный VS Code открываются;\n", "- smoke test завершился успешно и вернул ожидаемый результат;\n", - "- ты понимаешь назначение `MinIO`, `PostgreSQL`, `Spark`, `Trino`, `Jupyter`;\n", + "- ты понимаешь назначение `MinIO`, `PostgreSQL`, `Spark`, `Trino` и IDE;\n", "- ты знаешь, что первый маршрут диагностики начинается с `docker compose ps`, `docker compose logs -f ` и проверки UI.\n" ] }, diff --git a/tests/browser_ide.cjs b/tests/browser_ide.cjs new file mode 100644 index 0000000..7a8c210 --- /dev/null +++ b/tests/browser_ide.cjs @@ -0,0 +1,98 @@ +// Запуск через browser_ide.sh: настоящий notebook editor, локальное ядро и webview. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { chromium } = require('playwright'); + +const notebookName = 'lakehouse-browser-smoke.ipynb'; +const notebook = JSON.parse(fs.readFileSync( + path.join(__dirname, '../notebooks/01_environment_and_smoke_test.ipynb'), 'utf8', +)); +const smokeCell = notebook.cells.findIndex(cell => + cell.source.join('').includes('smoke_report = run_cluster_smoke(spark)'), +); + +async function openNotebook(page) { + await page.keyboard.press('Control+p'); + await page.keyboard.type(`/tmp/${notebookName}`); + await page.getByRole('option').filter({ hasText: notebookName }).first().waitFor(); + await page.keyboard.press('Enter'); + await page.getByRole('list', { name: /Notebook Use/ }).waitFor(); +} + +async function checkOutput(page) { + // Notebook editor виртуализирует ячейки: сначала показываем нужную ячейку. + const list = page.getByRole('list', { name: /Notebook Use/ }); + await list.press('Control+Home'); + for (let i = 0; i < smokeCell; i++) await list.press('ArrowDown'); + await page.getByRole('button', { + name: `Cell ${smokeCell + 1} of ${notebook.cells.length}`, exact: true, + }).waitFor(); + + for (let attempt = 0; attempt < 30; attempt++) { + for (const frame of page.frames()) { + const output = frame.getByText('result_sum=499999500000', { exact: false }); + if (await output.count() && await output.first().isVisible()) return; + } + await page.waitForTimeout(500); + } + throw new Error('Notebook webview did not render the expected Spark result'); +} + +(async () => { + assert(smokeCell >= 0, 'Module 1 must contain the cluster smoke cell'); + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }); + page.setDefaultTimeout(30000); + const webviewErrors = []; + page.on('console', message => { + if (/Error loading webview|Could not register.*service.?worker/i.test(message.text())) { + webviewErrors.push(message.text()); + } + }); + try { + await page.goto('http://localhost:8889', { waitUntil: 'domcontentloaded' }); + assert(await page.evaluate(() => isSecureContext && 'serviceWorker' in navigator)); + await page.getByRole('tree', { name: 'Files Explorer' }).waitFor(); + // Баннер появляется позже Explorer; при повторном тесте доверие уже сохранено. + const restricted = page.getByRole('banner').filter({ hasText: 'Restricted Mode' }); + await restricted.waitFor({ timeout: 10000 }).catch(error => { + if (error.name !== 'TimeoutError') throw error; + }); + if (await restricted.count()) { + await restricted.getByRole('button', { name: 'Manage', exact: true }).click(); + await page.getByRole('button', { name: 'Trust', exact: true }).click(); + await page.getByRole('button', { name: 'Close Modal Editor (Escape)' }).click(); + await restricted.waitFor({ state: 'hidden' }); + } + console.log('PASS: workspace trust enabled'); + + await openNotebook(page); + await page.getByRole('button', { name: 'Select Kernel', exact: true }).click(); + await page.getByRole('option', { name: 'Python Environments...' }).click(); + await page.getByRole('option').filter({ hasText: '/usr/bin/python3' }).click(); + console.log('PASS: notebook editor and local Python kernel selection'); + + await page.getByRole('button', { name: 'Run All', exact: true }).click(); + await page.getByRole('button', { name: 'Interrupt', exact: true }).waitFor(); + await page.getByRole('button', { name: 'Run All', exact: true }).waitFor({ timeout: 120000 }); + await checkOutput(page); + await page.keyboard.press('Control+s'); + console.log('PASS: Spark output rendered in notebook webview over HTTP localhost'); + + await page.getByRole('tab', { name: notebookName, exact: true }) + .getByRole('button', { name: 'Close (Ctrl+W)' }).click(); + await openNotebook(page); + await checkOutput(page); + assert.deepEqual(webviewErrors, []); + await page.screenshot({ path: '/artifacts/notebook.png' }); + await page.getByRole('tab', { name: notebookName, exact: true }) + .getByRole('button', { name: 'Close (Ctrl+W)' }).click(); + console.log('PASS: saved notebook reopened with its output'); + } catch (error) { + await page.screenshot({ path: '/artifacts/failure.png' }).catch(() => {}); + throw error; + } finally { + await browser.close(); + } +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/tests/browser_ide.sh b/tests/browser_ide.sh new file mode 100644 index 0000000..0a88df3 --- /dev/null +++ b/tests/browser_ide.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Проверка UI на Linux Docker Engine; зависимости браузера остаются в контейнере. +set -euo pipefail +cd "$(dirname "$0")/.." + +artifact_dir=$(mktemp -d /tmp/lakehouse-browser-check.XXXXXX) +echo "Browser artifacts: $artifact_dir" +docker compose exec -T ide cp /opt/work/01_environment_and_smoke_test.ipynb \ + /tmp/lakehouse-browser-smoke.ipynb + +docker run --rm --network host --shm-size=1g \ + -v "$PWD:/repo:ro" -v "$artifact_dir:/artifacts" \ + mcr.microsoft.com/playwright:v1.56.1-noble \ + bash -c 'npm install --prefix /tmp/browser-test --no-audit --no-fund playwright@1.56.1 >/dev/null && + NODE_PATH=/tmp/browser-test/node_modules node /repo/tests/browser_ide.cjs' + +docker compose exec -T ide python3 - <<'PY' +import json +from pathlib import Path + +path = Path('/tmp/lakehouse-browser-smoke.ipynb') +notebook = json.loads(path.read_text()) +cells = [cell for cell in notebook['cells'] if cell['cell_type'] == 'code'] +assert all(cell['execution_count'] is not None for cell in cells) +assert not any(output['output_type'] == 'error' + for cell in cells for output in cell.get('outputs', [])) +output = '\n'.join(''.join(item.get('text', [])) + for cell in cells for item in cell.get('outputs', [])) +assert 'master_url=spark://spark-master:7077' in output +assert 'result_sum=499999500000' in output +path.unlink() +print(f'PASS: all {len(cells)} code cells executed and saved without errors') +PY