├── .dockerignore ├── .eslintrc.js ├── .flake8 ├── .git-blame-ignore-revs ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yaml └── workflows │ ├── diff.yml │ ├── eslint.yml │ ├── jest.yml │ ├── playwright.yaml │ ├── publish.yml │ ├── test-docker-build.yaml │ ├── test.yml │ └── watch-dependencies.yaml ├── .gitignore ├── .hintrc ├── .pre-commit-config.yaml ├── .prettierignore ├── .readthedocs.yaml ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── babel.config.json ├── binderhub ├── __init__.py ├── __main__.py ├── _version.py ├── app.py ├── base.py ├── binderspawner_mixin.py ├── build.py ├── build_local.py ├── builder.py ├── config.py ├── event-schemas │ └── launch.json ├── events.py ├── handlers │ ├── __init__.py │ └── repoproviders.py ├── health.py ├── launcher.py ├── log.py ├── main.py ├── metrics.py ├── quota.py ├── ratelimit.py ├── registry.py ├── repoproviders.py ├── static │ ├── favicon.ico │ ├── images │ │ ├── badge.svg │ │ ├── badge_logo.svg │ │ ├── favicon │ │ │ ├── fail.ico │ │ │ ├── progress.ico │ │ │ └── success.ico │ │ ├── logo_social.png │ │ ├── logo_square.png │ │ ├── markdown-icon.svg │ │ └── rst-icon.svg │ ├── js │ │ ├── App.jsx │ │ ├── App.test.jsx │ │ ├── components │ │ │ ├── BuilderLauncher.jsx │ │ │ ├── ErrorPage.jsx │ │ │ ├── FaviconUpdater.jsx │ │ │ ├── HowItWorks.jsx │ │ │ ├── LinkGenerator.jsx │ │ │ ├── LoadingIndicator.css │ │ │ ├── LoadingIndicator.jsx │ │ │ ├── NBViewerIFrame.jsx │ │ │ └── Progress.jsx │ │ ├── index.d.ts │ │ ├── index.jsx │ │ ├── index.scss │ │ ├── pages │ │ │ ├── AboutPage.jsx │ │ │ ├── HomePage.jsx │ │ │ ├── HomePage.test.jsx │ │ │ ├── LoadingPage.jsx │ │ │ └── NotFoundPage.jsx │ │ └── spec.js │ └── logo.svg ├── templates │ └── page.html ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── http-record.api.github.com.gists.json │ ├── http-record.api.github.com.json │ ├── http-record.doi.org.json │ ├── http-record.nbviewer.jupyter.org.json │ ├── http-record.www.hydroshare.org.json │ ├── http-record.zenodo.org.json │ ├── test_app.py │ ├── test_auth.py │ ├── test_build.py │ ├── test_builder.py │ ├── test_eventlog.py │ ├── test_health.py │ ├── test_launcher.py │ ├── test_legacy.py │ ├── test_main.py │ ├── test_quota.py │ ├── test_ratelimit.py │ ├── test_registry.py │ ├── test_repoproviders.py │ ├── test_utils.py │ ├── test_version.py │ └── utils.py └── utils.py ├── ci ├── check_embedded_chart_code.py ├── common ├── publish └── refreeze ├── conftest.py ├── dev-requirements.txt ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── _static │ ├── custom.css │ └── images │ │ ├── architecture.png │ │ ├── favicon.png │ │ └── logo.png │ ├── _templates │ └── navigation.html │ ├── api.rst │ ├── authentication.rst │ ├── conf.py │ ├── contribute.md │ ├── cors.rst │ ├── customization │ └── index.rst │ ├── customizing.rst │ ├── debug.rst │ ├── developer │ ├── index.rst │ └── repoproviders.rst │ ├── eventlogging.rst │ ├── https.rst │ ├── index.rst │ ├── overview.rst │ ├── reference │ ├── app.rst │ ├── build.rst │ ├── builder.rst │ ├── launcher.rst │ ├── main.rst │ ├── ref-index.rst │ ├── registry.rst │ └── repoproviders.rst │ └── zero-to-binderhub │ ├── index.rst │ ├── ovh │ ├── generate_id_details.png │ └── new_project.png │ ├── private-gitlab-repo-token.png │ ├── private-repo-token.png │ ├── setup-binderhub.rst │ ├── setup-prerequisites.rst │ ├── setup-registry.rst │ └── turn-off.rst ├── examples ├── appendix │ ├── README.md │ ├── binderhub_config.py │ ├── extra_notebook_config.py │ ├── run-appendix │ ├── static │ │ └── custom.js │ └── templates │ │ ├── login.html │ │ └── page.html └── binder-api.py ├── helm-chart ├── .gitignore ├── LICENSE ├── README.md ├── binderhub │ ├── .helmignore │ ├── Chart.yaml │ ├── files │ │ └── binderhub_config.py │ ├── schema.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── container-builder │ │ │ └── daemonset.yaml │ │ ├── deployment.yaml │ │ ├── image-cleaner.yaml │ │ ├── ingress.yaml │ │ ├── pdb.yaml │ │ ├── rbac.yaml │ │ ├── secret.yaml │ │ └── service.yaml │ └── values.yaml ├── chartpress.yaml └── images │ └── binderhub │ ├── Dockerfile │ ├── README.md │ ├── requirements.in │ └── requirements.txt ├── integration-tests ├── conftest.py └── test_ui.py ├── js └── packages │ └── binderhub-client │ ├── README.md │ ├── lib │ └── index.js │ ├── package.json │ └── tests │ ├── fixtures │ └── fullbuild.eventsource │ ├── index.test.js │ └── utils.js ├── package.json ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── setupTests.js ├── testing ├── k8s-binder-k8s-hub │ ├── binderhub-chart+dind.yaml │ ├── binderhub-chart+pink.yaml │ ├── binderhub-chart-config-old.yaml │ ├── binderhub-chart-config.yaml │ ├── cm-insecure-registries-dind.yaml │ └── cm-insecure-registries-pink.yaml ├── local-binder-k8s-hub │ ├── binderhub_config.py │ ├── binderhub_config_auth_additions.py │ ├── install-jupyterhub-chart │ ├── jupyterhub-chart-config-auth-additions.yaml │ └── jupyterhub-chart-config.yaml ├── local-binder-local-hub │ ├── README.md │ ├── binderhub_config.py │ ├── jupyterhub_config.py │ └── requirements.txt └── local-binder-mocked-hub │ └── binderhub_config.py ├── tools ├── generate-json-schema.py ├── templates │ └── lint-and-validate-values.yaml └── validate-against-schema.py ├── tsconfig.json ├── versioneer.py └── webpack.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | helm-chart 2 | !helm-chart/images 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["eslint:recommended", "plugin:react/recommended"], 7 | ignorePatterns: ["dist"], 8 | overrides: [ 9 | { 10 | env: { 11 | node: true, 12 | }, 13 | files: [".eslintrc.{js,cjs}"], 14 | parserOptions: { 15 | sourceType: "script", 16 | }, 17 | }, 18 | { 19 | files: ["**/*.test.js", "**/*.test.jsx"], 20 | env: { 21 | jest: true, 22 | node: true, 23 | }, 24 | }, 25 | ], 26 | parserOptions: { 27 | ecmaVersion: "latest", 28 | sourceType: "module", 29 | }, 30 | plugins: ["react"], 31 | rules: { 32 | "react/react-in-jsx-scope": "off", 33 | "react/jsx-uses-react": "off", 34 | // Temporarily turn off prop-types 35 | "react/prop-types": "off", 36 | "no-unused-vars": ["error", { args: "after-used" }], 37 | }, 38 | ignorePatterns: [ 39 | "jupyterhub_fancy_profiles/static/*.js", 40 | "webpack.config.js", 41 | "babel.config.js", 42 | ], 43 | settings: { 44 | react: { 45 | version: "detect", 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | # flake8 is a Python linting tool run by pre-commit as declared by our 2 | # pre-commit-config.yaml file. 3 | # 4 | # This configuration is compatible with the autoformatter tool black, and 5 | # further relaxed to not bug us with too small details. 6 | # 7 | # flake8 configuration reference: 8 | # https://flake8.pycqa.org/en/latest/user/configuration.html 9 | # 10 | [flake8] 11 | max-line-length = 88 12 | extend-ignore = C, E, W 13 | 14 | # Adjustments to linting the binderhub repo 15 | builtins = c, load_subconfig 16 | exclude = versioneer.py,binderhub/_version.py 17 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # This file can be used to disregard certain commits when using `git blame`, and 2 | # GitHub will automatically use it for that by file name convention. 3 | # 4 | # Reference 1: https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt 5 | # Reference 2: https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view 6 | # 7 | 8 | # pre-commit: run requirements-txt-fixer 9 | da40927c5b187e37755901e675eee092e12ae2eb 10 | # pre-commit: run end-of-file-fixer 11 | b815163691eade7daeeb01549d0fc6bdf4a33186 12 | # pre-commit: run prettier 13 | 9fefdc42ec4d1701117f8f2880a80695eeaad5c9 14 | # pre-commit: run update-values-based-on-bindarspawner-mixin 15 | d32e9efa34cf0ca8880939e82ff01d3edc35705b 16 | # pre-commit: run isort 17 | f0fe0257506fd667279e97354f0207168aa1368c 18 | # pre-commit: run black 19 | bb047d8bce89655e197a7fdf2bdec2cb9c79940a 20 | # pre-commit: run pyupgrade 21 | 24da4e593461640aaa47dc008b1f657b92189085 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | binderhub/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # dependabot.yml reference: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 2 | # 3 | # Notes: 4 | # - Status and logs from dependabot are provided at 5 | # https://github.com/jupyterhub/binderhub/network/updates. 6 | # - YAML anchors are not supported here or in GitHub Workflows. 7 | # 8 | version: 2 9 | updates: 10 | # Maintain dependencies in our GitHub Workflows 11 | - package-ecosystem: github-actions 12 | directory: / 13 | labels: [ci] 14 | schedule: 15 | interval: monthly 16 | time: "05:00" 17 | timezone: Etc/UTC 18 | -------------------------------------------------------------------------------- /.github/workflows/diff.yml: -------------------------------------------------------------------------------- 1 | # This workflow provides a diff of the rendered Helm chart's templates with the 2 | # latest released dev version of the chart. 3 | # 4 | name: Helm diff 5 | 6 | on: 7 | pull_request: 8 | paths: 9 | - ".github/workflows/diff.yml" 10 | - "helm-chart/binderhub/**" 11 | push: 12 | paths: 13 | - ".github/workflows/diff.yml" 14 | - "helm-chart/binderhub/**" 15 | branches-ignore: 16 | - "dependabot/**" 17 | - "pre-commit-ci-update-config" 18 | - "update-*" 19 | workflow_dispatch: 20 | 21 | jobs: 22 | diff-rendered-templates: 23 | runs-on: ubuntu-22.04 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - uses: jupyterhub/action-k3s-helm@v4 30 | with: 31 | k3s-channel: stable 32 | metrics-enabled: false 33 | traefik-enabled: false 34 | docker-enabled: true 35 | 36 | - uses: actions/setup-python@v5 37 | with: 38 | python-version: "3.12" 39 | 40 | - name: Install helm diff plugin, update local chart dependencies 41 | run: | 42 | helm plugin install https://github.com/databus23/helm-diff 43 | helm dependency update ./helm-chart/binderhub 44 | 45 | - name: "Install latest released dev chart" 46 | run: | 47 | UPGRADE_FROM_VERSION=$(curl -sSL https://jupyterhub.github.io/helm-chart/info.json | jq -er '.binderhub.dev') 48 | 49 | # NOTE: We change the directory so binderhub the chart name won't be 50 | # misunderstood as the local folder name. 51 | # validation is disabled, because the config is for a different version! 52 | cd testing 53 | 54 | old_config="../testing/k8s-binder-k8s-hub/binderhub-chart-config-old.yaml" 55 | if [ -f "$old_config" ]; then 56 | echo "using old config" 57 | else 58 | old_config="../testing/k8s-binder-k8s-hub/binderhub-chart-config.yaml" 59 | fi 60 | 61 | helm install binderhub-test binderhub \ 62 | --values "$old_config" \ 63 | --repo https://jupyterhub.github.io/helm-chart/ \ 64 | --disable-openapi-validation \ 65 | --version=$UPGRADE_FROM_VERSION 66 | 67 | - name: "Helm diff latest released dev chart with local chart" 68 | run: | 69 | echo "NOTE: For the helm diff, we have not updated the Chart.yaml" 70 | echo " version or image tags using chartpress." 71 | echo 72 | 73 | helm diff upgrade binderhub-test helm-chart/binderhub \ 74 | --values testing/k8s-binder-k8s-hub/binderhub-chart-config.yaml \ 75 | --context=3 76 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # Lints the binderhub/static/js folders content with eslint, influenced by 2 | # ".eslintrc.js", by running "npm run lint" where "lint" is a command defined in 3 | # package.json. 4 | # 5 | name: eslint 6 | 7 | on: 8 | pull_request: 9 | paths: 10 | - ".github/workflows/eslint.yml" 11 | - ".eslintrc.js" 12 | - "package.json" 13 | - "binderhub/static/js/**" 14 | - "js/**" 15 | push: 16 | paths: 17 | - ".github/workflows/eslint.yml" 18 | - ".eslintrc.js" 19 | - "package.json" 20 | - "binderhub/static/js/**" 21 | - "js/**" 22 | branches-ignore: 23 | - "dependabot/**" 24 | - "pre-commit-ci-update-config" 25 | - "update-*" 26 | workflow_dispatch: 27 | 28 | jobs: 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - run: npm install 35 | 36 | - run: npm run lint 37 | -------------------------------------------------------------------------------- /.github/workflows/jest.yml: -------------------------------------------------------------------------------- 1 | # Runs jest based unit tests for frontend javascript and @jupyterhub/binderhub-client 2 | name: "JS Unit tests" 3 | 4 | on: 5 | pull_request: 6 | paths: 7 | - "binderhub/static/js/**" 8 | - "js/packages/binderhub-client/**" 9 | - ".github/workflows/jest.yml" 10 | push: 11 | paths: 12 | - "binderhub/static/js/**" 13 | - "js/packages/binderhub-client/**" 14 | - ".github/workflows/jest.yml" 15 | branches-ignore: 16 | - "dependabot/**" 17 | - "pre-commit-ci-update-config" 18 | - "update-*" 19 | workflow_dispatch: 20 | 21 | jobs: 22 | test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: "Setup dependencies" 28 | run: | 29 | npm install 30 | 31 | - name: "Run all unit tests" 32 | run: | 33 | npm test 34 | 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v5 37 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yaml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "**.md" 7 | - "**.rst" 8 | - "docs/**" 9 | - "examples/**" 10 | - ".github/workflows/**" 11 | - "!.github/workflows/playwright.yaml" 12 | push: 13 | paths-ignore: 14 | - "**.md" 15 | - "**.rst" 16 | - "docs/**" 17 | - "examples/**" 18 | - ".github/workflows/**" 19 | - "!.github/workflows/playwright.yaml" 20 | branches-ignore: 21 | - "dependabot/**" 22 | - "pre-commit-ci-update-config" 23 | - "update-*" 24 | workflow_dispatch: 25 | 26 | jobs: 27 | tests: 28 | runs-on: ubuntu-22.04 29 | timeout-minutes: 10 30 | 31 | permissions: 32 | contents: read 33 | env: 34 | GITHUB_ACCESS_TOKEN: "${{ secrets.github_token }}" 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - name: Setup OS level dependencies 40 | run: | 41 | sudo apt-get update 42 | sudo apt-get install --yes \ 43 | build-essential \ 44 | curl \ 45 | libcurl4-openssl-dev \ 46 | libssl-dev 47 | 48 | - uses: actions/setup-node@v4 49 | id: setup-node 50 | with: 51 | node-version: "22" 52 | 53 | - name: Cache npm 54 | uses: actions/cache@v4 55 | with: 56 | path: ~/.npm 57 | key: node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('**/package.json') }}-${{ github.job }} 58 | 59 | - name: Run webpack to build static assets 60 | run: | 61 | npm install 62 | npm run webpack 63 | 64 | - uses: actions/setup-python@v5 65 | id: setup-python 66 | with: 67 | python-version: "3.12" 68 | 69 | - name: Cache pip 70 | uses: actions/cache@v4 71 | with: 72 | path: ~/.cache/pip 73 | key: python-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/*requirements.txt') }}-${{ github.job }} 74 | 75 | - name: Setup test dependencies 76 | run: | 77 | npm i -g configurable-http-proxy 78 | 79 | pip install --no-binary pycurl -r dev-requirements.txt 80 | pip install -e . 81 | 82 | - name: Install playwright browser 83 | run: | 84 | playwright install firefox 85 | 86 | - name: Run playwright tests 87 | run: | 88 | py.test --cov=binderhub -s integration-tests/ 89 | 90 | - uses: actions/upload-artifact@v4 91 | if: always() 92 | with: 93 | name: playwright-traces 94 | path: test-results/ 95 | 96 | # Upload test coverage info to codecov 97 | - uses: codecov/codecov-action@v5 98 | -------------------------------------------------------------------------------- /.github/workflows/test-docker-build.yaml: -------------------------------------------------------------------------------- 1 | # This is a GitHub workflow defining a set of jobs with a set of steps. 2 | # ref: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions 3 | # 4 | name: Test docker multiarch build 5 | 6 | # Trigger the workflow's on all PRs and pushes so that other contributors can 7 | # run tests in their own forks. Avoid triggering this tests on changes not 8 | # influencing the image notably. 9 | on: 10 | pull_request: 11 | paths: 12 | - "helm-chart/images/**" 13 | - "helm-chart/chartpress.yaml" 14 | - ".github/workflows/test-docker-build.yaml" 15 | push: 16 | paths: 17 | - "helm-chart/images/**" 18 | - "helm-chart/chartpress.yaml" 19 | - ".github/workflows/test-docker-build.yaml" 20 | branches-ignore: 21 | - "dependabot/**" 22 | - "pre-commit-ci-update-config" 23 | - "update-*" 24 | workflow_dispatch: 25 | 26 | jobs: 27 | # This is a quick test to check the arm64 docker images based on: 28 | # - https://github.com/docker/build-push-action/blob/v2.3.0/docs/advanced/local-registry.md 29 | # - https://github.com/docker/build-push-action/blob/v2.3.0/docs/advanced/multi-platform.md 30 | build_images: 31 | runs-on: ubuntu-22.04 32 | steps: 33 | - uses: actions/checkout@v4 34 | with: 35 | # chartpress requires git history to set chart version and image tags 36 | # correctly 37 | fetch-depth: 0 38 | 39 | - uses: actions/setup-python@v5 40 | with: 41 | python-version: "3.12" 42 | 43 | - uses: actions/setup-node@v4 44 | # node required to build wheel 45 | with: 46 | node-version: "22" 47 | 48 | - name: Install chartpress 49 | run: pip install chartpress build 50 | 51 | - name: Build binderhub wheel 52 | run: python3 -m build --wheel . 53 | 54 | - name: Set up QEMU (for docker buildx) 55 | uses: docker/setup-qemu-action@v3 56 | 57 | - name: Set up Docker Buildx (for chartpress multi-arch builds) 58 | uses: docker/setup-buildx-action@v3 59 | 60 | - name: Build a multiple architecture Docker image 61 | run: | 62 | cd helm-chart 63 | chartpress \ 64 | --builder docker-buildx \ 65 | --platform linux/amd64 --platform linux/arm64 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Manually added parts to .gitignore 2 | # ---------------------------------- 3 | # 4 | 5 | # OS Stuff 6 | .DS_Store 7 | 8 | # Node stuff 9 | node_modules/ 10 | package-lock.json 11 | 12 | # Built files 13 | binderhub/static/dist 14 | helm-chart/binderhub/charts 15 | helm-chart/binderhub/requirements.lock 16 | testing/k8s-binder-k8s-hub/binderhub-chart-config-remote.yaml 17 | ci/id_rsa 18 | 19 | # Instructions we download 20 | k8s.txt 21 | helm.txt 22 | 23 | # Federation data page 24 | docs/federation/data-federation.txt 25 | 26 | # Editors etc 27 | .vscode/ 28 | 29 | 30 | # Python .gitignore from https://github.com/github/gitignore/blob/HEAD/Python.gitignore 31 | # ------------------------------------------------------------------------------------- 32 | # 33 | # Byte-compiled / optimized / DLL files 34 | __pycache__/ 35 | *.py[cod] 36 | *$py.class 37 | 38 | # C extensions 39 | *.so 40 | 41 | # Distribution / packaging 42 | .Python 43 | build/ 44 | develop-eggs/ 45 | dist/ 46 | downloads/ 47 | eggs/ 48 | .eggs/ 49 | lib64/ 50 | parts/ 51 | sdist/ 52 | var/ 53 | wheels/ 54 | pip-wheel-metadata/ 55 | share/python-wheels/ 56 | *.egg-info/ 57 | .installed.cfg 58 | *.egg 59 | MANIFEST 60 | 61 | # PyInstaller 62 | # Usually these files are written by a python script from a template 63 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 64 | *.manifest 65 | *.spec 66 | 67 | # Installer logs 68 | pip-log.txt 69 | pip-delete-this-directory.txt 70 | 71 | # Unit test / coverage reports 72 | htmlcov/ 73 | .tox/ 74 | .nox/ 75 | .coverage 76 | .coverage.* 77 | .cache 78 | nosetests.xml 79 | coverage.xml 80 | *.cover 81 | *.py,cover 82 | .hypothesis/ 83 | .pytest_cache/ 84 | coverage 85 | 86 | # Translations 87 | *.mo 88 | *.pot 89 | 90 | # Django stuff: 91 | *.log 92 | local_settings.py 93 | db.sqlite3 94 | db.sqlite3-journal 95 | 96 | # Flask stuff: 97 | instance/ 98 | .webassets-cache 99 | 100 | # Scrapy stuff: 101 | .scrapy 102 | 103 | # Sphinx documentation 104 | docs/_build/ 105 | 106 | # PyBuilder 107 | target/ 108 | 109 | # Jupyter Notebook 110 | .ipynb_checkpoints 111 | 112 | # IPython 113 | profile_default/ 114 | ipython_config.py 115 | 116 | # pyenv 117 | .python-version 118 | 119 | # pipenv 120 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 121 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 122 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 123 | # install all needed dependencies. 124 | #Pipfile.lock 125 | 126 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 127 | __pypackages__/ 128 | 129 | # Celery stuff 130 | celerybeat-schedule 131 | celerybeat.pid 132 | 133 | # SageMath parsed files 134 | *.sage.py 135 | 136 | # Environments 137 | .env 138 | .venv 139 | env/ 140 | venv/ 141 | ENV/ 142 | env.bak/ 143 | venv.bak/ 144 | 145 | # Spyder project settings 146 | .spyderproject 147 | .spyproject 148 | 149 | # Rope project settings 150 | .ropeproject 151 | 152 | # mkdocs documentation 153 | /site 154 | 155 | # mypy 156 | .mypy_cache/ 157 | .dmypy.json 158 | dmypy.json 159 | 160 | # Pyre type checker 161 | .pyre/ 162 | -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "development" 4 | ], 5 | "hints": { 6 | "meta-viewport": "off" 7 | } 8 | } 9 | 10 | 11 | //This is the configuration file for the VSCode extension called webhint, it identifies accessibilty errors in code 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # pre-commit is a tool to perform a predefined set of tasks manually and/or 2 | # automatically before git commits are made. 3 | # 4 | # Config reference: https://pre-commit.com/#pre-commit-configyaml---top-level 5 | # 6 | # Common tasks 7 | # 8 | # - Run on all files: pre-commit run --all-files 9 | # - Register git hooks: pre-commit install --install-hooks 10 | # 11 | repos: 12 | # Autoformat: Python code, syntax patterns are modernized 13 | - repo: https://github.com/asottile/pyupgrade 14 | rev: v3.19.1 15 | hooks: 16 | - id: pyupgrade 17 | args: 18 | - --py38-plus 19 | 20 | # Autoformat: Python code 21 | - repo: https://github.com/psf/black 22 | rev: 25.1.0 23 | hooks: 24 | - id: black 25 | # args are not passed, but see the config in pyproject.toml 26 | 27 | # Autoformat: Python code 28 | - repo: https://github.com/pycqa/isort 29 | rev: 6.0.0 30 | hooks: 31 | - id: isort 32 | # args are not passed, but see the config in pyproject.toml 33 | 34 | # Generated code: 35 | # An entry in helm-chart/binderhub/values.yaml should be generated based on 36 | # binderhub/binderspawner_mixin.py. See ci/check_embedded_chart_code.py for 37 | # more details. 38 | - repo: local 39 | hooks: 40 | - id: update-values-based-on-binderspawner-mixin 41 | name: Update helm-chart/binderhub/values.yaml based on binderhub/binderspawner_mixin.py 42 | language: python 43 | additional_dependencies: ["ruamel.yaml"] 44 | entry: python ci/check_embedded_chart_code.py 45 | args: 46 | - --update 47 | files: binderhub/binderspawner_mixin.py|helm-chart/binderhub/values.yaml 48 | pass_filenames: false 49 | 50 | # Autoformat: js, html, markdown, yaml, json 51 | - repo: https://github.com/pre-commit/mirrors-prettier 52 | rev: v4.0.0-alpha.8 53 | hooks: 54 | - id: prettier 55 | exclude_types: 56 | # These are excluded initially as pre-commit was added but can 57 | # absolutely be enabled later. If so, we should consider having a 58 | # separate run of pre-commit where we configure a line spacing of 4 59 | # for these file formats. 60 | - html 61 | 62 | # Misc autoformatting and linting 63 | - repo: https://github.com/pre-commit/pre-commit-hooks 64 | rev: v5.0.0 65 | hooks: 66 | - id: end-of-file-fixer 67 | exclude_types: [svg] 68 | - id: check-case-conflict 69 | - id: check-executables-have-shebangs 70 | - id: requirements-txt-fixer 71 | # exclude ci/refreeze generated requirements.txt 72 | exclude: ^.*images\/.*\/requirements\.txt$ 73 | 74 | # Lint: Python code 75 | - repo: https://github.com/PyCQA/flake8 76 | rev: "7.1.1" 77 | hooks: 78 | - id: flake8 79 | 80 | # versioneer.py is excluded for being an imported dependency we opt to not 81 | # modify ourselves. This is also set in .flake8 for consistency. 82 | exclude: versioneer.py|binderhub/_version.py 83 | 84 | # pre-commit.ci config reference: https://pre-commit.ci/#configuration 85 | ci: 86 | autoupdate_schedule: monthly 87 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | helm-chart/binderhub/templates/ 2 | http-record.doi.org.json 3 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Configuration on how ReadTheDocs (RTD) builds our documentation 2 | # ref: https://readthedocs.org/projects/binderhub/ 3 | # ref: https://docs.readthedocs.io/en/stable/config-file/v2.html 4 | # 5 | version: 2 6 | 7 | sphinx: 8 | configuration: docs/source/conf.py 9 | 10 | build: 11 | os: ubuntu-22.04 12 | tools: 13 | python: "3.11" 14 | 15 | python: 16 | install: 17 | - requirements: docs/requirements.txt 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This documentation is available at https://binderhub.readthedocs.io/en/latest/contribute.html. 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) Project Jupyter Contributors 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include binderhub/static * 2 | recursive-include binderhub/templates * 3 | recursive-include binderhub/event-schemas * 4 | graft testing 5 | graft doc 6 | include LICENSE 7 | include package.json 8 | include requirements.txt 9 | include versioneer.py 10 | include webpack.config.js 11 | include binderhub/_version.py 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [BinderHub](https://github.com/jupyterhub/binderhub) 2 | 3 | [![Documentation Status](https://img.shields.io/readthedocs/binderhub?logo=read-the-docs)](https://binderhub.readthedocs.io/en/latest/) 4 | [![GitHub Workflow Status - Test](https://img.shields.io/github/actions/workflow/status/jupyterhub/binderhub/test.yml?logo=github&label=tests)](https://github.com/jupyterhub/binderhub/actions) 5 | [![Latest chart development release](https://img.shields.io/badge/dynamic/json.svg?label=latest&url=https://jupyterhub.github.io/helm-chart/info.json&query=$.binderhub.latest&colorB=orange)](https://jupyterhub.github.io/helm-chart/) 6 | [![GitHub](https://img.shields.io/badge/issue_tracking-github-blue.svg)](https://github.com/jupyterhub/binderhub/issues) 7 | [![Discourse](https://img.shields.io/badge/help_forum-discourse-blue.svg)](https://discourse.jupyter.org/c/binder/binderhub) 8 | [![Gitter](https://img.shields.io/badge/social_chat-gitter-blue.svg)](https://gitter.im/jupyterhub/binder) 9 | [![Contribute](https://img.shields.io/badge/I_want_to_contribute!-grey?logo=jupyter)](https://binderhub.readthedocs.io/en/latest/contribute.html) 10 | 11 | ## What is BinderHub? 12 | 13 | **BinderHub** allows you to `BUILD` and `REGISTER` a Docker image from a 14 | Git repository, then `CONNECT` with JupyterHub, allowing you to create a 15 | public IP address that allows users to interact with the code and 16 | environment within a live JupyterHub instance. You can select a specific 17 | branch name, commit, or tag to serve. 18 | 19 | BinderHub ties together: 20 | 21 | - [JupyterHub](https://github.com/jupyterhub/jupyterhub) to provide a scalable 22 | system for authenticating users and spawning single user Jupyter Notebook 23 | servers, and 24 | - [Repo2Docker](https://github.com/jupyter/repo2docker) which generates a Docker 25 | image using a Git repository hosted online. 26 | 27 | BinderHub is built with Python, kubernetes, tornado, npm, webpack, and 28 | sphinx. 29 | 30 | ## Documentation 31 | 32 | For more information about the architecture, use, and setup of 33 | BinderHub, see [the BinderHub 34 | documentation](https://binderhub.readthedocs.io). 35 | 36 | ## Contributing 37 | 38 | To contribute to the BinderHub project you can work on: 39 | 40 | - [answering questions others have](https://discourse.jupyter.org/), 41 | - writing documentation, 42 | - designing the user interface, or 43 | - writing code. 44 | 45 | To see how to build the documentation, edit the user interface or modify 46 | the code see [the contribution 47 | guide](https://github.com/jupyterhub/binderhub/blob/HEAD/CONTRIBUTING.md). 48 | 49 | ## Installation 50 | 51 | **BinderHub** is based on Python 3, it's currently only kept updated on GitHub. 52 | However, it can be installed using `pip`: 53 | 54 | pip install git+https://github.com/jupyterhub/binderhub 55 | 56 | See [the BinderHub documentation](https://binderhub.readthedocs.io) for 57 | a detailed guide on setting up your own BinderHub server. 58 | 59 | ## Why BinderHub? 60 | 61 | Collections of Jupyter notebooks are becoming more common in scientific 62 | research and data science. The ability to serve these collections on 63 | demand enhances the usefulness of these notebooks. 64 | 65 | ## Who is BinderHub for? 66 | 67 | - **Users** who want to easily interact with computational environments that 68 | others have created. 69 | - **Authors** who want to create links that allow users to immediately interact 70 | with a computational enviroment that you specify. 71 | - **Deployers** who want to create their own BinderHub to run on whatever 72 | hardware they choose. 73 | 74 | ## License 75 | 76 | See `LICENSE` file in this repository. 77 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | ["@babel/preset-react", { "runtime": "automatic" }] 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /binderhub/__init__.py: -------------------------------------------------------------------------------- 1 | # next three lines were added by versioneer 2 | from . import _version 3 | 4 | __version__ = _version.get_versions()["version"] 5 | -------------------------------------------------------------------------------- /binderhub/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | from binderhub.app import main 3 | 4 | main() 5 | -------------------------------------------------------------------------------- /binderhub/config.py: -------------------------------------------------------------------------------- 1 | from .base import BaseHandler 2 | 3 | 4 | class ConfigHandler(BaseHandler): 5 | """Serve config""" 6 | 7 | def generate_config(self): 8 | config = dict() 9 | for repo_provider_class_alias, repo_provider_class in self.settings[ 10 | "repo_providers" 11 | ].items(): 12 | config[repo_provider_class_alias] = repo_provider_class.labels 13 | return config 14 | 15 | async def get(self): 16 | self.write(self.generate_config()) 17 | -------------------------------------------------------------------------------- /binderhub/event-schemas/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "binderhub.jupyter.org/launch", 3 | "version": 5, 4 | "title": "BinderHub Launch Events", 5 | "description": "BinderHub emits this event whenever a new repo is launched", 6 | "type": "object", 7 | "properties": { 8 | "provider": { 9 | "enum": [ 10 | "GitHub", 11 | "Gist", 12 | "GitLab", 13 | "Git", 14 | "Zenodo", 15 | "Figshare", 16 | "Hydroshare", 17 | "Dataverse", 18 | "CKAN" 19 | ], 20 | "description": "Provider for the repository being launched" 21 | }, 22 | "spec": { 23 | "type": "string", 24 | "description": "Provider specification for the repo being launched. Usually, /" 25 | }, 26 | "ref": { 27 | "type": "string", 28 | "description": "Resolved reference for the repo at the time of launch" 29 | }, 30 | "status": { 31 | "enum": ["success", "failure"], 32 | "description": "Success/Failure of the launch" 33 | }, 34 | "build_token": { 35 | "type": "boolean", 36 | "description": "Whether a build token was used for the launch" 37 | }, 38 | "origin": { 39 | "type": "string", 40 | "description": "BinderHub host where the event originated" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /binderhub/handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/binderhub/handlers/__init__.py -------------------------------------------------------------------------------- /binderhub/handlers/repoproviders.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from ..base import BaseHandler 4 | 5 | 6 | class RepoProvidersHandlers(BaseHandler): 7 | """Serve config""" 8 | 9 | async def get(self): 10 | config = [ 11 | repo_provider_class.display_config 12 | for repo_provider_class in self.settings["repo_providers"].values() 13 | ] 14 | self.set_header("Content-type", "application/json") 15 | self.write(json.dumps(config)) 16 | -------------------------------------------------------------------------------- /binderhub/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main handler classes for requests 3 | """ 4 | 5 | import time 6 | 7 | import jwt 8 | from tornado.httputil import url_concat 9 | from tornado.web import authenticated 10 | 11 | from . import __version__ as binder_version 12 | from .base import BaseHandler 13 | 14 | 15 | class UIHandler(BaseHandler): 16 | """ 17 | Responds to most UI Page Requests 18 | """ 19 | 20 | def initialize(self): 21 | self.opengraph_title = self.settings["default_opengraph_title"] 22 | self.page_config = {} 23 | return super().initialize() 24 | 25 | @authenticated 26 | def get(self): 27 | repoproviders_display_config = [ 28 | repo_provider_class.display_config 29 | for repo_provider_class in self.settings["repo_providers"].values() 30 | ] 31 | self.page_config |= { 32 | "baseUrl": self.settings["base_url"], 33 | "badgeBaseUrl": self.get_badge_base_url(), 34 | "logoUrl": self.static_url("logo.svg"), 35 | "logoWidth": "320px", 36 | "repoProviders": repoproviders_display_config, 37 | "aboutMessage": self.settings["about_message"], 38 | "bannerHtml": self.settings["banner_message"], 39 | "binderVersion": binder_version, 40 | } 41 | self.render_template( 42 | "page.html", 43 | page_config=self.page_config, 44 | extra_footer_scripts=self.settings["extra_footer_scripts"], 45 | opengraph_title=self.opengraph_title, 46 | ) 47 | 48 | 49 | class RepoLaunchUIHandler(UIHandler): 50 | """ 51 | Responds to /v2/ launch URLs only 52 | 53 | Forwards to UIHandler, but puts out an opengraph_title for social previews 54 | """ 55 | 56 | def initialize(self, repo_provider): 57 | self.repo_provider = repo_provider 58 | return super().initialize() 59 | 60 | @authenticated 61 | def get(self, provider_id, _escaped_spec): 62 | prefix = "/v2/" + provider_id 63 | spec = self.get_spec_from_request(prefix) 64 | 65 | build_token = jwt.encode( 66 | { 67 | "exp": int(time.time()) + self.settings["build_token_expires_seconds"], 68 | "aud": f"{provider_id}/{spec}", 69 | "origin": self.token_origin(), 70 | }, 71 | key=self.settings["build_token_secret"], 72 | algorithm="HS256", 73 | ) 74 | self.page_config["buildToken"] = build_token 75 | self.opengraph_title = ( 76 | f"{self.repo_provider.display_config['displayName']}: {spec}" 77 | ) 78 | return super().get() 79 | 80 | 81 | class LegacyRedirectHandler(BaseHandler): 82 | """Redirect handler from legacy Binder""" 83 | 84 | @authenticated 85 | def get(self, user, repo, urlpath=None): 86 | url = f"/v2/gh/{user}/{repo}/master" 87 | if urlpath is not None and urlpath.strip("/"): 88 | url = url_concat(url, dict(urlpath=urlpath)) 89 | self.redirect(url) 90 | -------------------------------------------------------------------------------- /binderhub/metrics.py: -------------------------------------------------------------------------------- 1 | from prometheus_client import CONTENT_TYPE_LATEST, REGISTRY, generate_latest 2 | 3 | from .base import BaseHandler 4 | 5 | 6 | class MetricsHandler(BaseHandler): 7 | # demote logging of 200 responses to debug-level 8 | log_success_debug = True 9 | 10 | async def get(self): 11 | self.set_header("Content-Type", CONTENT_TYPE_LATEST) 12 | self.write(generate_latest(REGISTRY)) 13 | -------------------------------------------------------------------------------- /binderhub/ratelimit.py: -------------------------------------------------------------------------------- 1 | """Rate limiting utilities""" 2 | 3 | import time 4 | 5 | from traitlets import Dict, Float, Integer, default 6 | from traitlets.config import LoggingConfigurable 7 | 8 | 9 | class RateLimitExceeded(Exception): 10 | """Exception raised when rate limit is exceeded""" 11 | 12 | 13 | class RateLimiter(LoggingConfigurable): 14 | """Class representing a collection of rate limits 15 | 16 | Has one method: `.increment(key)` which should be called when 17 | a given actor is attempting to consume a resource. 18 | `key` can be any hashable (e.g. request ip address). 19 | Each `key` has its own rate limit counter. 20 | 21 | If the rate limit is exhausted, a RateLimitExceeded exception is raised, 22 | otherwise a summary of the current rate limit remaining is returned. 23 | 24 | Rate limits are reset to zero at the end of `period_seconds`, 25 | not a sliding window, 26 | so the entire rate limit can be consumed instantly 27 | """ 28 | 29 | period_seconds = Integer( 30 | 3600, 31 | config=True, 32 | help="""The rate limit window""", 33 | ) 34 | 35 | limit = Integer( 36 | 10, 37 | config=True, 38 | help="""The number of requests to allow within period_seconds""", 39 | ) 40 | 41 | clean_seconds = Integer( 42 | 600, 43 | config=True, 44 | help="""Interval on which to clean out old limits. 45 | 46 | Avoids memory growth of unused limits 47 | """, 48 | ) 49 | 50 | _limits = Dict() 51 | 52 | _last_cleaned = Float() 53 | 54 | @default("_last_cleaned") 55 | def _default_last_cleaned(self): 56 | return self.time() 57 | 58 | def _clean_limits(self): 59 | now = self.time() 60 | self._last_cleaned = now 61 | self._limits = { 62 | key: limit for key, limit in self._limits.items() if limit["reset"] > now 63 | } 64 | 65 | @staticmethod 66 | def time(): 67 | """Mostly here to enable override in tests""" 68 | return time.time() 69 | 70 | def increment(self, key): 71 | """Check rate limit for a key 72 | 73 | key: key for recording rate limit. Each key tracks a different rate limit. 74 | Returns: {"remaining": int_remaining, "reset": int_timestamp} 75 | Raises: RateLimitExceeded if the request would exceed the rate limit. 76 | """ 77 | now = int(self.time()) 78 | if now - self._last_cleaned > self.clean_seconds: 79 | self._clean_limits() 80 | 81 | if key not in self._limits or self._limits[key]["reset"] < now: 82 | # no limit recorded, or reset expired 83 | self._limits[key] = { 84 | "remaining": self.limit, 85 | "reset": now + self.period_seconds, 86 | } 87 | limit = self._limits[key] 88 | # keep decrementing, so we have a track of excess requests 89 | # which indicate abuse 90 | limit["remaining"] -= 1 91 | if limit["remaining"] < 0: 92 | seconds_until_reset = int(limit["reset"] - now) 93 | raise RateLimitExceeded( 94 | f"Rate limit exceeded (by {-limit['remaining']}) for {key!r}, reset in {seconds_until_reset}s." 95 | ) 96 | return limit 97 | -------------------------------------------------------------------------------- /binderhub/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/binderhub/static/favicon.ico -------------------------------------------------------------------------------- /binderhub/static/images/badge.svg: -------------------------------------------------------------------------------- 1 | launchlaunchbinderbinder -------------------------------------------------------------------------------- /binderhub/static/images/badge_logo.svg: -------------------------------------------------------------------------------- 1 | launchlaunchbinderbinder -------------------------------------------------------------------------------- /binderhub/static/images/favicon/fail.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/binderhub/static/images/favicon/fail.ico -------------------------------------------------------------------------------- /binderhub/static/images/favicon/progress.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/binderhub/static/images/favicon/progress.ico -------------------------------------------------------------------------------- /binderhub/static/images/favicon/success.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/binderhub/static/images/favicon/success.ico -------------------------------------------------------------------------------- /binderhub/static/images/logo_social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/binderhub/static/images/logo_social.png -------------------------------------------------------------------------------- /binderhub/static/images/logo_square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/binderhub/static/images/logo_square.png -------------------------------------------------------------------------------- /binderhub/static/images/markdown-icon.svg: -------------------------------------------------------------------------------- 1 | m -------------------------------------------------------------------------------- /binderhub/static/images/rst-icon.svg: -------------------------------------------------------------------------------- 1 | .rst -------------------------------------------------------------------------------- /binderhub/static/js/App.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | 3 | import { App } from "./App"; 4 | import { memoryLocation } from "wouter/memory-location"; 5 | 6 | test("render Homepage", () => { 7 | render(); 8 | expect( 9 | screen.queryByText( 10 | /Turn a Git repo into a collection of interactive notebooks/, 11 | ), 12 | ).toBeInTheDocument(); 13 | }); 14 | 15 | test("render About page", () => { 16 | const { hook } = memoryLocation({ path: "/about" }); 17 | render(); 18 | expect(screen.queryByText(/This is the about message/)).toBeInTheDocument(); 19 | expect(screen.queryByText(/v123.456/)).toBeInTheDocument(); 20 | }); 21 | 22 | test("render Not Found page", () => { 23 | const { hook } = memoryLocation({ path: "/not-found" }); 24 | render(); 25 | expect(screen.queryByText(/Not Found/)).toBeInTheDocument(); 26 | }); 27 | 28 | test("renders loading page", () => { 29 | const { hook } = memoryLocation({ path: "/v2/gh/user/repo/main" }); 30 | render(); 31 | expect(screen.queryByText(/Launching your Binder/)).toBeInTheDocument(); 32 | }); 33 | 34 | test("renders loading page with trailing slash", () => { 35 | const { hook } = memoryLocation({ path: "/v2/gh/user/repo/main/" }); 36 | render(); 37 | expect(screen.queryByText(/Launching your Binder/)).toBeInTheDocument(); 38 | }); 39 | 40 | test("renders error for misconfigured repo", () => { 41 | const { hook } = memoryLocation({ path: "/v2/gh/userrep/main/" }); 42 | render(); 43 | expect(screen.queryByText(/Not Found/)).toBeInTheDocument(); 44 | }); 45 | 46 | test("renders loading page with trailing slash", () => { 47 | const { hook } = memoryLocation({ 48 | path: "/v2/zenodo/10.5281/zenodo.3242074/", 49 | }); 50 | render(); 51 | expect(screen.queryByText(/Launching your Binder/)).toBeInTheDocument(); 52 | }); 53 | -------------------------------------------------------------------------------- /binderhub/static/js/components/ErrorPage.jsx: -------------------------------------------------------------------------------- 1 | export function ErrorPage({ title, errorMessage }) { 2 | return ( 3 | <> 4 |
5 |

{title}

6 | 7 |

{errorMessage}

8 |
9 |
10 |
11 |

12 | questions? 13 |
14 | join the{" "} 15 | discussion, 16 | read the{" "} 17 | docs, see 18 | the code 19 |

20 |
21 |
22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /binderhub/static/js/components/FaviconUpdater.jsx: -------------------------------------------------------------------------------- 1 | import ProgressIcon from "../../images/favicon/progress.ico"; 2 | import FailIcon from "../../images/favicon/fail.ico"; 3 | import SuccessIcon from "../../images/favicon/success.ico"; 4 | 5 | import { PROGRESS_STATES } from "./Progress.jsx"; 6 | 7 | /** 8 | * @typedef {object} FaviconUpdaterProps 9 | * @prop {PROGRESS_STATES} progressState 10 | * @param {FaviconUpdaterProps} props 11 | */ 12 | export function FaviconUpdater({ progressState }) { 13 | let icon; 14 | switch (progressState) { 15 | case PROGRESS_STATES.FAILED: { 16 | icon = FailIcon; 17 | break; 18 | } 19 | case PROGRESS_STATES.SUCCESS: { 20 | icon = SuccessIcon; 21 | break; 22 | } 23 | case PROGRESS_STATES.BUILDING: 24 | case PROGRESS_STATES.PUSHING: 25 | case PROGRESS_STATES.LAUNCHING: { 26 | icon = ProgressIcon; 27 | break; 28 | } 29 | } 30 | 31 | return ; 32 | } 33 | -------------------------------------------------------------------------------- /binderhub/static/js/components/HowItWorks.jsx: -------------------------------------------------------------------------------- 1 | export function HowItWorks() { 2 | return ( 3 |
4 |

How it works

5 | 6 |
7 |
8 | 15 | 1 16 | 17 |
18 |
19 |

Enter your repository information

20 | Provide in the above form a URL or a GitHub repository that contains 21 | Jupyter notebooks, as well as a branch, tag, or commit hash. Launch 22 | will build your Binder repository. If you specify a path to a notebook 23 | file, the notebook will be opened in your browser after building. 24 |
25 |
26 | 27 |
28 |
29 | 36 | 2 37 | 38 |
39 |
40 |

We build a Docker image of your repository

41 | Binder will search for a dependency file, such as requirements.txt or 42 | environment.yml, in the repository's root directory ( 43 | 44 | more details on more complex dependencies in documentation 45 | 46 | ). The dependency files will be used to build a Docker image. If an 47 | image has already been built for the given repository, it will not be 48 | rebuilt. If a new commit has been made, the image will automatically 49 | be rebuilt. 50 |
51 |
52 | 53 |
54 |
55 | 62 | 3 63 | 64 |
65 |
66 |

Interact with your notebooks in a live environment!

A{" "} 67 | JupyterHub{" "} 68 | server will host your repository's contents. We offer you a reusable 69 | link and badge to your live repository that you can easily share with 70 | others. 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /binderhub/static/js/components/LoadingIndicator.css: -------------------------------------------------------------------------------- 1 | /*CSS to generate rotating concentric circles for a loading screen. Thanks to 2 | https://ihatetomatoes.net for initial code templates.*/ 3 | 4 | #loader { 5 | display: block; 6 | position: relative; 7 | left: 50%; 8 | top: 50%; 9 | width: 150px; 10 | height: 150px; 11 | margin: -20px 0 0 -75px; 12 | border-radius: 50%; 13 | border: 7px solid transparent; 14 | border-top-color: #f5a252; 15 | animation: spin 2s linear infinite; 16 | z-index: 1001; 17 | } 18 | 19 | #loader:before { 20 | content: ""; 21 | position: absolute; 22 | top: 5px; 23 | left: 5px; 24 | right: 5px; 25 | bottom: 5px; 26 | border-radius: 50%; 27 | border: 7px solid transparent; 28 | border-top-color: #579aca; 29 | animation: spin 3s linear infinite; 30 | } 31 | 32 | #loader:after { 33 | content: ""; 34 | position: absolute; 35 | top: 15px; 36 | left: 15px; 37 | right: 15px; 38 | bottom: 15px; 39 | border-radius: 50%; 40 | border: 7px solid transparent; 41 | border-top-color: #e66581; 42 | animation: spin 1.5s linear infinite; 43 | } 44 | 45 | @keyframes spin { 46 | 0% { 47 | transform: rotateZ(0deg); 48 | } 49 | 100% { 50 | transform: rotateZ(360deg); 51 | } 52 | } 53 | 54 | #loader.error, 55 | #loader.error:after, 56 | #loader.error:before { 57 | border-top-color: red !important; 58 | } 59 | 60 | #loader.error { 61 | animation: spin 30s linear infinite !important; 62 | } 63 | 64 | #loader.error:after { 65 | animation: spin 10s linear infinite !important; 66 | } 67 | 68 | #loader.error:before { 69 | animation: spin 20s linear infinite !important; 70 | } 71 | 72 | .paused, 73 | .paused:after, 74 | .paused:before { 75 | animation-play-state: paused !important; 76 | } 77 | -------------------------------------------------------------------------------- /binderhub/static/js/components/LoadingIndicator.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import "./LoadingIndicator.css"; 3 | import { PROGRESS_STATES } from "./Progress.jsx"; 4 | /** 5 | * List of help messages we will cycle through randomly in the loading page 6 | */ 7 | const HELP_MESSAGES = [ 8 | 'New to Binder? Check out the Binder Documentation for more information.', 9 | 'You can learn more about building your own Binder repositories in the Binder community documentation.', 10 | 'We use the repo2docker tool to automatically build the environment in which to run your code.', 11 | 'Take a look at the full list of configuration files supported by repo2docker.', 12 | 'Need more than just a Jupyter notebook? You can customize the user interface.', 13 | 'Take a look at our gallery of example repositories.', 14 | "If a repository takes a long time to launch, it is usually because Binder needs to create the environment for the first time.", 15 | 'The tool that powers this page is called BinderHub. It is an open source tool that you can deploy yourself.', 16 | 'The Binder team has a site reliability guide that talks about what it is like to run a BinderHub.', 17 | 'You can connect with the Binder community in the Jupyter community forum.', 18 | "Empty log? Notebook not loading? Maybe your ad blocker is interfering. Consider adding this site to the list of trusted sources.", 19 | "Your launch may take longer the first few times a repository is used. This is because our machine needs to create your environment.", 20 | 'Read our advice for speeding up your Binder launch.', 21 | ]; 22 | 23 | /** 24 | * @typedef {object} LoadingIndicatorProps 25 | * @prop {PROGRESS_STATES} progressState 26 | * @param {LoadingIndicatorProps} props 27 | */ 28 | export function LoadingIndicator({ progressState }) { 29 | const [currentMessage, setCurrentMessage] = useState(HELP_MESSAGES[0]); 30 | 31 | useEffect(() => { 32 | const intervalId = setInterval(() => { 33 | const newMessage = 34 | HELP_MESSAGES[Math.floor(Math.random() * HELP_MESSAGES.length)]; 35 | setCurrentMessage(newMessage); 36 | }, 6 * 1000); 37 | 38 | return () => clearInterval(intervalId); 39 | }, []); 40 | 41 | return ( 42 |
43 |
47 | {progressState === PROGRESS_STATES.FAILED ? ( 48 |

49 | Launching your Binder failed! See the logs below for more information. 50 |

51 | ) : ( 52 | <> 53 |

Launching your Binder...

54 |
55 |

56 |
57 | 58 | )} 59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /binderhub/static/js/components/NBViewerIFrame.jsx: -------------------------------------------------------------------------------- 1 | import { Spec } from "../spec"; 2 | 3 | /** 4 | * @typedef {object} NBViewerIFrameProps 5 | * @prop {Spec} spec 6 | * @param {NBViewerIFrameProps} props 7 | * @returns 8 | */ 9 | export function NBViewerIFrame({ spec }) { 10 | // We only support GitHub links as preview right now 11 | if (!spec.buildSpec.startsWith("gh/")) { 12 | return; 13 | } 14 | 15 | const [_, org, repo, ref] = spec.buildSpec.split("/"); 16 | 17 | let urlPath = decodeURI(spec.urlPath); 18 | // Handle cases where urlPath starts with a `/` 19 | urlPath = urlPath.replace(/^\//, ""); 20 | let filePath = ""; 21 | if (urlPath.startsWith("doc/tree/")) { 22 | filePath = urlPath.replace(/^doc\/tree\//, ""); 23 | } else if (urlPath.startsWith("tree/")) { 24 | filePath = urlPath.replace(/^tree\//, ""); 25 | } 26 | 27 | let url; 28 | // TODO: The nbviewer url should be configurable 29 | if (filePath) { 30 | url = `https://nbviewer.jupyter.org/github/${org}/${repo}/blob/${ref}/${filePath}`; 31 | } else { 32 | url = `https://nbviewer.jupyter.org/github/${org}/${repo}/tree/${ref}`; 33 | } 34 | 35 | return ( 36 |
37 |

38 | Here is a non-interactive preview on{" "} 39 | 40 | nbviewer 41 | {" "} 42 | while we start a server for you.
43 | Your binder will open automatically when it is ready. 44 |

45 | 50 |
51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /binderhub/static/js/components/Progress.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @enum {string} 3 | */ 4 | export const PROGRESS_STATES = { 5 | WAITING: "Waiting", 6 | BUILDING: "Building", 7 | PUSHING: "Pushing", 8 | LAUNCHING: "Launching", 9 | SUCCESS: "Success", 10 | FAILED: "Failed", 11 | }; 12 | 13 | const progressDisplay = {}; 14 | (progressDisplay[PROGRESS_STATES.WAITING] = { 15 | precursors: [], 16 | widthPercent: "10", 17 | label: "Waiting", 18 | className: "text-bg-danger", 19 | }), 20 | (progressDisplay[PROGRESS_STATES.BUILDING] = { 21 | precursors: [PROGRESS_STATES.WAITING], 22 | widthPercent: "50", 23 | label: "Building", 24 | className: "text-bg-warning", 25 | }); 26 | 27 | progressDisplay[PROGRESS_STATES.PUSHING] = { 28 | precursors: [PROGRESS_STATES.WAITING, PROGRESS_STATES.BUILDING], 29 | widthPercent: "30", 30 | label: "Pushing", 31 | className: "text-bg-info", 32 | }; 33 | 34 | progressDisplay[PROGRESS_STATES.LAUNCHING] = { 35 | precursors: [ 36 | PROGRESS_STATES.WAITING, 37 | PROGRESS_STATES.BUILDING, 38 | PROGRESS_STATES.PUSHING, 39 | ], 40 | widthPercent: "10", 41 | label: "Launching", 42 | className: "text-bg-success", 43 | }; 44 | 45 | progressDisplay[PROGRESS_STATES.SUCCESS] = 46 | progressDisplay[PROGRESS_STATES.LAUNCHING]; 47 | 48 | progressDisplay[PROGRESS_STATES.FAILED] = { 49 | precursors: [], 50 | widthPercent: "100", 51 | label: "Failed", 52 | className: "text-bg-danger", 53 | }; 54 | 55 | /** 56 | * @typedef {object} ProgressProps 57 | * @prop {PROGRESS_STATES} progressState 58 | * @param {ProgressProps} props 59 | */ 60 | export function Progress({ progressState }) { 61 | return ( 62 |
67 | {progressState === null 68 | ? "" 69 | : progressDisplay[progressState].precursors 70 | .concat([progressState]) 71 | .map((s) => ( 72 |
77 | {progressDisplay[s].label} 78 |
79 | ))} 80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /binderhub/static/js/index.d.ts: -------------------------------------------------------------------------------- 1 | // Tell typescript to be quiet about .ico files we use for favicons 2 | declare module "*.ico"; 3 | -------------------------------------------------------------------------------- /binderhub/static/js/index.jsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import { App } from "./App"; 3 | 4 | const root = createRoot(document.getElementById("root")); 5 | root.render(); 6 | -------------------------------------------------------------------------------- /binderhub/static/js/index.scss: -------------------------------------------------------------------------------- 1 | @import "bootstrap/scss/functions"; 2 | 3 | // Theming overrides 4 | $primary: rgb(223, 132, 41); 5 | $custom-colors: ( 6 | "primary": $primary, 7 | ); 8 | 9 | // Import these after theming overrides so they pick up these variables 10 | @import "bootstrap/scss/variables"; 11 | @import "bootstrap/scss/variables-dark"; 12 | @import "bootstrap/scss/maps"; 13 | @import "bootstrap/scss/mixins"; 14 | @import "bootstrap/scss/utilities"; 15 | @import "bootstrap/scss/root"; 16 | @import "bootstrap/scss/reboot"; 17 | 18 | // Merge the maps 19 | $theme-colors: map-merge($theme-colors, $custom-colors); 20 | 21 | @import "bootstrap/scss/bootstrap"; 22 | 23 | // Font choices 24 | 25 | body { 26 | font-family: "Clear Sans"; 27 | font-weight: 300; 28 | } 29 | 30 | form { 31 | font-weight: 400; 32 | } 33 | 34 | .btn-primary, 35 | .btn-primary:hover { 36 | color: $white; 37 | } 38 | 39 | a { 40 | text-decoration: none; 41 | } 42 | 43 | // Could not replicate this style with just utility classes unfortunately 44 | .circle-point { 45 | border: 5px solid; 46 | padding: 2px 9px; 47 | border-radius: 50%; 48 | font-weight: bold; 49 | } 50 | 51 | .form-label { 52 | font-size: 1rem; 53 | } 54 | 55 | .jumbotron { 56 | margin-bottom: 100px; 57 | } 58 | 59 | .bg-custom-dark { 60 | background-color: rgb(235, 236, 237); 61 | } 62 | 63 | @import "bootstrap-icons/font/bootstrap-icons.css"; 64 | -------------------------------------------------------------------------------- /binderhub/static/js/pages/AboutPage.jsx: -------------------------------------------------------------------------------- 1 | export function AboutPage({ aboutMessage, binderVersion }) { 2 | return ( 3 |
4 |

BinderHub

5 |
6 |

7 | This website is powered by{" "} 8 | BinderHub v 9 | {binderVersion} 10 |

11 | {aboutMessage && ( 12 |

13 | )} 14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /binderhub/static/js/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import { LinkGenerator } from "../components/LinkGenerator.jsx"; 2 | import { BuilderLauncher } from "../components/BuilderLauncher.jsx"; 3 | import { HowItWorks } from "../components/HowItWorks.jsx"; 4 | import { useEffect, useState } from "react"; 5 | import { FaviconUpdater } from "../components/FaviconUpdater.jsx"; 6 | import { Spec, LaunchSpec } from "../spec.js"; 7 | 8 | /** 9 | * @typedef {object} HomePageProps 10 | * @prop {import("../App.jsx").Provider[]} providers 11 | * @prop {URL} publicBaseUrl 12 | * @prop {URL} baseUrl 13 | * @param {HomePageProps} props 14 | */ 15 | export function HomePage({ providers, publicBaseUrl, baseUrl }) { 16 | const defaultProvider = providers[0]; 17 | const [selectedProvider, setSelectedProvider] = useState(defaultProvider); 18 | const [repo, setRepo] = useState(""); 19 | const [ref, setRef] = useState(""); 20 | const [urlPath, setUrlPath] = useState(""); 21 | const [isLaunching, setIsLaunching] = useState(false); 22 | const [spec, setSpec] = useState(""); 23 | const [progressState, setProgressState] = useState(null); 24 | 25 | useEffect(() => { 26 | const encodedRepo = selectedProvider.repo.urlEncode 27 | ? encodeURIComponent(repo) 28 | : repo; 29 | let actualRef = ""; 30 | if (selectedProvider.ref.enabled) { 31 | actualRef = ref !== "" ? ref : selectedProvider.ref.default; 32 | } 33 | setSpec( 34 | new Spec( 35 | `${selectedProvider.id}/${encodedRepo}/${actualRef}`, 36 | new LaunchSpec(urlPath), 37 | ), 38 | ); 39 | }, [selectedProvider, repo, ref, urlPath]); 40 | 41 | return ( 42 | <> 43 |
44 |
Turn a Git repo into a collection of interactive notebooks
45 |

46 | Have a repository full of Jupyter notebooks? With Binder, open those 47 | notebooks in an executable environment, making your code immediately 48 | reproducible by anyone, anywhere. 49 |

50 |

51 | New to Binder? Get started with a{" "} 52 | 57 | Zero-to-Binder tutorial 58 | {" "} 59 | in Julia, Python, or R. 60 |

61 |
62 | 77 | 86 | 87 | 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /binderhub/static/js/pages/LoadingPage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { BuilderLauncher } from "../components/BuilderLauncher.jsx"; 3 | import { useParams, useSearch } from "wouter"; 4 | import { NBViewerIFrame } from "../components/NBViewerIFrame.jsx"; 5 | import { LoadingIndicator } from "../components/LoadingIndicator.jsx"; 6 | import { FaviconUpdater } from "../components/FaviconUpdater.jsx"; 7 | import { LaunchSpec, Spec } from "../spec.js"; 8 | import { ErrorPage } from "../components/ErrorPage.jsx"; 9 | 10 | /** 11 | * @typedef {object} LoadingPageProps 12 | * @prop {URL} baseUrl 13 | * @prop {string?} buildToken 14 | * @prop {import("../App.jsx").Provider} provider 15 | * @param {LoadingPageProps} props 16 | * @returns 17 | */ 18 | export function LoadingPage({ baseUrl, buildToken, provider }) { 19 | const [progressState, setProgressState] = useState(null); 20 | 21 | const params = useParams(); 22 | const partialSpec = params["0"]; 23 | const buildSpec = `${provider.id}/${partialSpec}`; 24 | 25 | const searchParams = new URLSearchParams(useSearch()); 26 | 27 | const [isLaunching, setIsLaunching] = useState(false); 28 | 29 | const spec = new Spec(buildSpec, LaunchSpec.fromSearchParams(searchParams)); 30 | const formatError = partialSpec.match(provider.spec.validateRegex) === null; 31 | 32 | useEffect(() => { 33 | if (!formatError) { 34 | // Start launching after the DOM has fully loaded 35 | setTimeout(() => setIsLaunching(true), 1); 36 | } 37 | }, []); 38 | 39 | if (formatError) { 40 | return ( 41 | 47 | ); 48 | } 49 | 50 | return ( 51 | <> 52 | 53 | 63 | 64 | 65 | 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /binderhub/static/js/pages/NotFoundPage.jsx: -------------------------------------------------------------------------------- 1 | export function NotFoundPage() { 2 | return ( 3 | <> 4 |
5 |

404: Not Found

6 |
7 |
8 |
9 |

10 | questions? 11 |
12 | join the{" "} 13 | discussion, 14 | read the{" "} 15 | docs, see 16 | the code 17 |

18 |
19 |
20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /binderhub/static/js/spec.js: -------------------------------------------------------------------------------- 1 | export class LaunchSpec { 2 | /** 3 | * 4 | * @param {string} urlPath Path inside the Jupyter server to redirect the user to after launching 5 | */ 6 | constructor(urlPath) { 7 | this.urlPath = urlPath; 8 | // Ensure no leading / here 9 | this.urlPath = this.urlPath.replace(/^\/*/, ""); 10 | } 11 | 12 | /** 13 | * Return a URL to redirect user to for use with this launch specification 14 | * 15 | * @param {URL} serverUrl Fully qualified URL to a running Jupyter Server 16 | * @param {string} token Authentication token to pass to the Jupyter Server 17 | * 18 | * @returns {URL} 19 | */ 20 | getJupyterServerRedirectUrl(serverUrl, token) { 21 | const redirectUrl = new URL(this.urlPath, serverUrl); 22 | redirectUrl.searchParams.append("token", token); 23 | return redirectUrl; 24 | } 25 | 26 | /** 27 | * Create a LaunchSpec from given query parameters in the URL 28 | * 29 | * Handles backwards compatible parameters as needed. 30 | * 31 | * @param {URLSearchParams} searchParams 32 | * 33 | * @returns {LaunchSpec} 34 | */ 35 | static fromSearchParams(searchParams) { 36 | let urlPath = searchParams.get("urlpath"); 37 | if (urlPath === null) { 38 | urlPath = ""; 39 | } 40 | 41 | // Handle legacy parameters for opening URLs after launching 42 | // labpath and filepath 43 | if (searchParams.has("labpath")) { 44 | // Trim trailing / on file paths 45 | const filePath = searchParams.get("labpath").replace(/(\/$)/g, ""); 46 | urlPath = `doc/tree/${encodeURI(filePath)}`; 47 | } else if (searchParams.has("filepath")) { 48 | // Trim trailing / on file paths 49 | const filePath = searchParams.get("filepath").replace(/(\/$)/g, ""); 50 | urlPath = `tree/${encodeURI(filePath)}`; 51 | } 52 | 53 | return new LaunchSpec(urlPath); 54 | } 55 | } 56 | 57 | /** 58 | * A full binder specification 59 | * 60 | * Includes a *build* specification (determining what is built), and a 61 | * *launch* specification (determining what is launched). 62 | */ 63 | export class Spec { 64 | /** 65 | * @param {string} buildSpec Build specification, passed directly to binderhub API 66 | * @param {LaunchSpec} launchSpec Launch specification, determining what is launched 67 | */ 68 | constructor(buildSpec, launchSpec) { 69 | this.buildSpec = buildSpec; 70 | this.launchSpec = launchSpec; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /binderhub/static/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 12 | 14 | 16 | 19 | 23 | 26 | 27 | 28 | 29 | 30 | 32 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /binderhub/templates/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}Binder{% endblock %} 6 | {% block meta_social %} 7 | {# Social media previews #} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% endblock meta_social %} 16 | 17 | 18 | 19 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | {% if extra_footer_scripts %} 30 | {% for script in extra_footer_scripts|dictsort %} 31 | 34 | {% endfor %} 35 | {% endif %} 36 | 37 | 38 | -------------------------------------------------------------------------------- /binderhub/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/binderhub/tests/__init__.py -------------------------------------------------------------------------------- /binderhub/tests/http-record.doi.org.json: -------------------------------------------------------------------------------- 1 | # Please don't replace me. We rely on being redirected in the Zenodo provider 2 | # which our mockinng setup doesn't replicate 3 | -------------------------------------------------------------------------------- /binderhub/tests/test_app.py: -------------------------------------------------------------------------------- 1 | """Exercise the binderhub entrypoint""" 2 | 3 | import sys 4 | from subprocess import check_output 5 | 6 | import pytest 7 | from traitlets import TraitError 8 | 9 | from binderhub.app import BinderHub 10 | from binderhub.repoproviders import GitHubRepoProvider, GitLabRepoProvider, RepoProvider 11 | 12 | 13 | def test_help(): 14 | check_output([sys.executable, "-m", "binderhub", "-h"]) 15 | 16 | 17 | def test_help_all(): 18 | check_output([sys.executable, "-m", "binderhub", "--help-all"]) 19 | 20 | 21 | def test_repo_providers(): 22 | # Check that repo_providers property is validated by traitlets.validate 23 | 24 | b = BinderHub() 25 | 26 | class Provider(RepoProvider): 27 | pass 28 | 29 | # Setting providers that inherit from 'RepoProvider` should be allowed 30 | b.repo_providers = dict(gh=GitHubRepoProvider, gl=GitLabRepoProvider) 31 | b.repo_providers = dict(p=Provider) 32 | 33 | class BadProvider: 34 | pass 35 | 36 | # Setting providers that don't inherit from 'RepoProvider` should raise an error 37 | wrong_repo_providers = [GitHubRepoProvider, {}, "GitHub", BadProvider] 38 | for repo_providers in wrong_repo_providers: 39 | with pytest.raises(TraitError): 40 | b.repo_providers = repo_providers 41 | -------------------------------------------------------------------------------- /binderhub/tests/test_builder.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from binderhub.builder import _generate_build_name, _get_image_basename_and_tag 4 | 5 | 6 | @pytest.mark.parametrize( 7 | "fullname,basename,tag", 8 | [ 9 | ( 10 | "jupyterhub/k8s-binderhub:0.2.0-a2079a5", 11 | "jupyterhub/k8s-binderhub", 12 | "0.2.0-a2079a5", 13 | ), 14 | ("jupyterhub/jupyterhub", "jupyterhub/jupyterhub", "latest"), 15 | ("gcr.io/project/image:tag", "project/image", "tag"), 16 | ("weirdregistry.com/image:tag", "image", "tag"), 17 | ( 18 | "gitlab-registry.example.com/group/project:some-tag", 19 | "group/project", 20 | "some-tag", 21 | ), 22 | ( 23 | "gitlab-registry.example.com/group/project/image:latest", 24 | "group/project/image", 25 | "latest", 26 | ), 27 | ( 28 | "gitlab-registry.example.com/group/project/my/image:rc1", 29 | "group/project/my/image", 30 | "rc1", 31 | ), 32 | ], 33 | ) 34 | def test_image_basename_resolution(fullname, basename, tag): 35 | result_basename, result_tag = _get_image_basename_and_tag(fullname) 36 | assert result_basename == basename 37 | assert result_tag == tag 38 | 39 | 40 | @pytest.mark.parametrize( 41 | "ref,build_slug", 42 | [ 43 | # a long ref, no special characters at critical positions 44 | ("3035124.v3.0", "dataverse-dvn-2ftjclkp"), 45 | # with ref_length=6 this has a full stop that gets escaped to a - 46 | # as the last character, this used to cause an error 47 | ("20460.v1.0", "dataverse-s6-2fde95rt"), 48 | # short ref, should just work and need no special processing 49 | ("123456", "dataverse-s6-2fde95rt"), 50 | ], 51 | ) 52 | def test_build_name(build_slug, ref): 53 | # Build names have to be usable as pod names, which means they have to 54 | # be usable as hostnames as well. 55 | build_name = _generate_build_name(build_slug, ref) 56 | 57 | last_char = build_name[-1] 58 | assert last_char not in ("-", "_", ".") 59 | -------------------------------------------------------------------------------- /binderhub/tests/test_eventlog.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import tempfile 5 | 6 | import jsonschema 7 | import pytest 8 | 9 | from binderhub.events import EventLog 10 | 11 | 12 | def test_register_invalid(): 13 | """ 14 | Test registering invalid schemas fails 15 | """ 16 | el = EventLog() 17 | with pytest.raises(jsonschema.SchemaError): 18 | el.register_schema( 19 | { 20 | # Totally invalid 21 | "properties": True 22 | } 23 | ) 24 | 25 | with pytest.raises(ValueError): 26 | el.register_schema({"properties": {}}) 27 | 28 | with pytest.raises(ValueError): 29 | el.register_schema( 30 | { 31 | "$id": "something", 32 | "$version": 1, 33 | "properties": {"timestamp": {"type": "string"}}, 34 | } 35 | ) 36 | 37 | 38 | def test_emit_event(): 39 | """ 40 | Test emitting launch events works 41 | """ 42 | schema = { 43 | "$id": "test/test", 44 | "version": 1, 45 | "properties": { 46 | "something": {"type": "string"}, 47 | }, 48 | } 49 | with tempfile.NamedTemporaryFile() as f: 50 | handler = logging.FileHandler(f.name) 51 | el = EventLog(handlers_maker=lambda el: [handler]) 52 | el.register_schema(schema) 53 | 54 | el.emit( 55 | "test/test", 56 | 1, 57 | { 58 | "something": "blah", 59 | }, 60 | ) 61 | handler.flush() 62 | 63 | f.seek(0) 64 | event_capsule = json.load(f) 65 | 66 | assert "timestamp" in event_capsule 67 | # Remove timestamp from capsule when checking equality, since it is gonna vary 68 | del event_capsule["timestamp"] 69 | assert event_capsule == { 70 | "schema": "test/test", 71 | "version": 1, 72 | "something": "blah", 73 | } 74 | 75 | 76 | def test_emit_event_badschema(): 77 | """ 78 | Test failure when event doesn't match schema 79 | """ 80 | schema = { 81 | "$id": "test/test", 82 | "version": 1, 83 | "properties": { 84 | "something": {"type": "string"}, 85 | "status": {"enum": ["success", "failure"]}, 86 | }, 87 | } 88 | with tempfile.NamedTemporaryFile() as f: 89 | handler = logging.FileHandler(f.name) 90 | el = EventLog(handlers_maker=lambda el: [handler]) 91 | el.register_schema(schema) 92 | 93 | with pytest.raises(jsonschema.ValidationError): 94 | el.emit("test/test", 1, {"something": "blah", "status": "not-in-enum"}) 95 | 96 | 97 | async def test_provider_completeness(app): 98 | """ 99 | Test we add an entry in the launch schema for all providers 100 | """ 101 | repo_providers = {rp.name.default_value for rp in app.repo_providers.values()} 102 | 103 | launch_schema_path = os.path.join( 104 | os.path.dirname(__file__), "..", "event-schemas", "launch.json" 105 | ) 106 | 107 | with open(launch_schema_path) as f: 108 | launch_schema = json.load(f) 109 | 110 | assert repo_providers == set(launch_schema["properties"]["provider"]["enum"]) 111 | -------------------------------------------------------------------------------- /binderhub/tests/test_health.py: -------------------------------------------------------------------------------- 1 | """Test health handler""" 2 | 3 | from .utils import async_requests 4 | 5 | 6 | async def test_basic_health(app): 7 | r = await async_requests.get(app.url + "/health") 8 | 9 | assert r.status_code == 200 10 | results = r.json() 11 | 12 | assert results["ok"] 13 | assert "checks" in results 14 | 15 | checks = results["checks"] 16 | 17 | assert {"service": "JupyterHub API", "ok": True} in checks 18 | 19 | # find the result of the quota check 20 | quota_check = [c for c in checks if c["service"] == "Pod quota"][0] 21 | assert quota_check 22 | assert quota_check["quota"] is None 23 | for key in ("build_pods", "total_pods", "user_pods"): 24 | assert key in quota_check 25 | 26 | assert ( 27 | quota_check["total_pods"] 28 | == quota_check["build_pods"] + quota_check["user_pods"] 29 | ) 30 | 31 | # HEAD requests should work as well 32 | r = await async_requests.head(app.url + "/health") 33 | assert r.status_code == 200 34 | -------------------------------------------------------------------------------- /binderhub/tests/test_launcher.py: -------------------------------------------------------------------------------- 1 | """Test launcher""" 2 | 3 | import pytest 4 | from tornado import web 5 | 6 | from binderhub.launcher import Launcher 7 | 8 | 9 | async def my_pre_launch_hook(launcher, *args): 10 | raise web.HTTPError( 11 | 400, "Launch is not possible with parameters: " + ",".join(args) 12 | ) 13 | 14 | 15 | async def test_pre_launch_hook(): 16 | launcher = Launcher(create_user=False, pre_launch_hook=my_pre_launch_hook) 17 | parameters = ["image", "test_user", "test_server", "repo_url"] 18 | with pytest.raises(web.HTTPError) as excinfo: 19 | _ = await launcher.launch(*parameters) 20 | assert excinfo.value.status_code == 400 21 | message = excinfo.value.log_message 22 | assert parameters == message.split(":", 1)[-1].lstrip().split(",") 23 | -------------------------------------------------------------------------------- /binderhub/tests/test_legacy.py: -------------------------------------------------------------------------------- 1 | """Test legacy redirects""" 2 | 3 | import pytest 4 | 5 | from .utils import async_requests 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "old_url, new_url", 10 | [ 11 | ( 12 | "/repo/binderhub-ci-repos/requirements", 13 | "/v2/gh/binderhub-ci-repos/requirements/master", 14 | ), 15 | ( 16 | "/repo/binderhub-ci-repos/requirements/", 17 | "/v2/gh/binderhub-ci-repos/requirements/master", 18 | ), 19 | ( 20 | "/repo/binderhub-ci-repos/requirements/notebooks/index.ipynb", 21 | "/v2/gh/binderhub-ci-repos/requirements/master?urlpath=%2Fnotebooks%2Findex.ipynb", 22 | ), 23 | ], 24 | ) 25 | async def test_legacy_redirect(app, old_url, new_url): 26 | r = await async_requests.get(app.url + old_url, allow_redirects=False) 27 | assert r.status_code == 302 28 | assert r.headers["location"] == new_url 29 | -------------------------------------------------------------------------------- /binderhub/tests/test_main.py: -------------------------------------------------------------------------------- 1 | """Test main handlers""" 2 | 3 | import json 4 | import time 5 | from urllib.parse import quote 6 | 7 | import jwt 8 | import pytest 9 | from bs4 import BeautifulSoup 10 | 11 | from .utils import async_requests 12 | 13 | 14 | @pytest.mark.parametrize( 15 | "origin,host,expected_origin", 16 | [ 17 | ("https://my.host", "my.host", "my.host"), 18 | ("https://my.origin", "my.host", "my.origin"), 19 | (None, "my.host", "my.host"), 20 | ], 21 | ) 22 | async def test_build_token_origin(app, origin, host, expected_origin): 23 | provider_spec = "git/{}/HEAD".format( 24 | quote( 25 | "https://github.com/binderhub-ci-repos/cached-minimal-dockerfile", 26 | safe="", 27 | ) 28 | ) 29 | uri = f"/v2/{provider_spec}" 30 | headers = {} 31 | if origin: 32 | headers["Origin"] = origin 33 | if host: 34 | headers["Host"] = host 35 | 36 | r = await async_requests.get(app.url + uri, headers=headers) 37 | 38 | soup = BeautifulSoup(r.text, "html5lib") 39 | script_tag = soup.select_one("head > script") 40 | page_config_str = ( 41 | script_tag.string.strip().removeprefix("window.pageConfig = ").removesuffix(";") 42 | ) 43 | print(page_config_str) 44 | page_config = json.loads(page_config_str) 45 | print(page_config) 46 | 47 | assert "buildToken" in page_config 48 | 49 | build_token = page_config["buildToken"] 50 | payload = jwt.decode( 51 | build_token, 52 | audience=provider_spec, 53 | options=dict(verify_signature=False), 54 | ) 55 | assert payload["aud"] == provider_spec 56 | assert payload["origin"] == expected_origin 57 | assert time.time() < payload["exp"] < time.time() + 7200 58 | -------------------------------------------------------------------------------- /binderhub/tests/test_quota.py: -------------------------------------------------------------------------------- 1 | """Test launch quotas""" 2 | 3 | import concurrent.futures 4 | import json 5 | from unittest import mock 6 | 7 | import pytest 8 | 9 | from binderhub.quota import KubernetesLaunchQuota, LaunchQuotaExceeded 10 | 11 | 12 | @pytest.fixture 13 | def mock_pod_list_resp(): 14 | r = mock.MagicMock() 15 | r.read.return_value = json.dumps( 16 | { 17 | "items": [ 18 | { 19 | "spec": { 20 | "containers": [ 21 | {"image": "example.org/test/kubernetes_quota:1.2.3"} 22 | ], 23 | }, 24 | }, 25 | { 26 | "spec": { 27 | "containers": [ 28 | {"image": "example.org/test/kubernetes_quota:latest"} 29 | ], 30 | }, 31 | }, 32 | { 33 | "spec": { 34 | "containers": [{"image": "example.org/test/other:abc"}], 35 | }, 36 | }, 37 | ] 38 | } 39 | ) 40 | f = concurrent.futures.Future() 41 | f.set_result(r) 42 | return f 43 | 44 | 45 | async def test_kubernetes_quota_none(mock_pod_list_resp): 46 | quota = KubernetesLaunchQuota(api=mock.MagicMock(), executor=mock.MagicMock()) 47 | quota.executor.submit.return_value = mock_pod_list_resp 48 | 49 | r = await quota.check_repo_quota( 50 | "example.org/test/kubernetes_quota", {}, "repo.url" 51 | ) 52 | assert r is None 53 | 54 | 55 | async def test_kubernetes_quota_allowed(mock_pod_list_resp): 56 | quota = KubernetesLaunchQuota(api=mock.MagicMock(), executor=mock.MagicMock()) 57 | quota.executor.submit.return_value = mock_pod_list_resp 58 | 59 | r = await quota.check_repo_quota( 60 | "example.org/test/kubernetes_quota", {"quota": 3}, "repo.url" 61 | ) 62 | assert r.total == 3 63 | assert r.matching == 2 64 | assert r.quota == 3 65 | 66 | 67 | async def test_kubernetes_quota_total_exceeded(mock_pod_list_resp): 68 | quota = KubernetesLaunchQuota( 69 | api=mock.MagicMock(), executor=mock.MagicMock(), total_quota=3 70 | ) 71 | quota.executor.submit.return_value = mock_pod_list_resp 72 | 73 | with pytest.raises(LaunchQuotaExceeded) as excinfo: 74 | await quota.check_repo_quota( 75 | "example.org/test/kubernetes_quota", {}, "repo.url" 76 | ) 77 | assert excinfo.value.message == "Too many users on this BinderHub! Try again soon." 78 | assert excinfo.value.quota == 3 79 | assert excinfo.value.used == 3 80 | assert excinfo.value.status == "pod_quota" 81 | 82 | 83 | async def test_kubernetes_quota_repo_exceeded(mock_pod_list_resp): 84 | quota = KubernetesLaunchQuota(api=mock.MagicMock(), executor=mock.MagicMock()) 85 | quota.executor.submit.return_value = mock_pod_list_resp 86 | 87 | with pytest.raises(LaunchQuotaExceeded) as excinfo: 88 | await quota.check_repo_quota( 89 | "example.org/test/kubernetes_quota", {"quota": 2}, "repo.url" 90 | ) 91 | assert excinfo.value.message == "Too many users running repo.url! Try again soon." 92 | assert excinfo.value.quota == 2 93 | assert excinfo.value.used == 2 94 | assert excinfo.value.status == "repo_quota" 95 | -------------------------------------------------------------------------------- /binderhub/tests/test_ratelimit.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from binderhub.ratelimit import RateLimiter, RateLimitExceeded 6 | 7 | 8 | def test_rate_limit(): 9 | r = RateLimiter(limit=10, period_seconds=60) 10 | assert r._limits == {} 11 | now = r.time() 12 | reset = int(now) + 60 13 | with mock.patch.object(r, "time", lambda: now): 14 | limit = r.increment("1.2.3.4") 15 | assert limit == { 16 | "remaining": 9, 17 | "reset": reset, 18 | } 19 | assert r._limits == { 20 | "1.2.3.4": limit, 21 | } 22 | for i in range(1, 10): 23 | limit = r.increment("1.2.3.4") 24 | assert limit == { 25 | "remaining": 9 - i, 26 | "reset": reset, 27 | } 28 | 29 | for i in range(5): 30 | with pytest.raises(RateLimitExceeded): 31 | r.increment("1.2.3.4") 32 | 33 | assert r._limits["1.2.3.4"]["remaining"] == -5 34 | 35 | 36 | def test_rate_limit_expires(): 37 | r = RateLimiter(limit=10, period_seconds=60) 38 | assert r._limits == {} 39 | now = r.time() 40 | 41 | for i in range(5): 42 | limit = r.increment("1.2.3.4") 43 | 44 | # now expire period, should get fresh limit 45 | with mock.patch.object(r, "time", lambda: now + 65): 46 | limit = r.increment("1.2.3.4") 47 | assert limit == { 48 | "remaining": 9, 49 | "reset": int(now) + 65 + 60, 50 | } 51 | 52 | 53 | def test_rate_limit_clean(): 54 | r = RateLimiter(limit=10, period_seconds=60) 55 | assert r._limits == {} 56 | now = r.time() 57 | 58 | r.increment("1.2.3.4") 59 | 60 | with mock.patch.object(r, "time", lambda: now + 30): 61 | limit2 = r.increment("4.3.2.1") 62 | 63 | # force clean, shouldn't expire 64 | r._last_cleaned = now - r.clean_seconds 65 | with mock.patch.object(r, "time", lambda: now + 35): 66 | limit2 = r.increment("4.3.2.1") 67 | 68 | assert "1.2.3.4" in r._limits 69 | 70 | # force clean again, should expire 71 | r._last_cleaned = now - r.clean_seconds 72 | with mock.patch.object(r, "time", lambda: now + 65): 73 | limit2 = r.increment("4.3.2.1") 74 | 75 | assert r._last_cleaned == now + 65 76 | assert "1.2.3.4" not in r._limits 77 | assert "4.3.2.1" in r._limits 78 | # 4.3.2.1 hasn't expired, still consuming rate limit 79 | assert limit2["remaining"] == 7 80 | -------------------------------------------------------------------------------- /binderhub/tests/test_version.py: -------------------------------------------------------------------------------- 1 | """Test version handler""" 2 | 3 | import pytest 4 | 5 | from binderhub import __version__ as binder_version 6 | 7 | from .utils import async_requests 8 | 9 | 10 | @pytest.mark.remote 11 | async def test_versions_handler(app): 12 | # Check that the about page loads 13 | r = await async_requests.get(app.url + "/versions") 14 | assert r.status_code == 200 15 | 16 | data = r.json() 17 | # builder_info is different for KubernetesExecutor and LocalRepo2dockerBuild 18 | try: 19 | import repo2docker 20 | 21 | allowed_builder_info = [{"repo2docker-version": repo2docker.__version__}] 22 | except ImportError: 23 | allowed_builder_info = [] 24 | allowed_builder_info.append({"build_image": app.build_image}) 25 | 26 | assert data["builder_info"] in allowed_builder_info 27 | assert data["binderhub"].split("+")[0] == binder_version.split("+")[0] 28 | -------------------------------------------------------------------------------- /ci/check_embedded_chart_code.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # FIXME: We currently have some code duplicated in 4 | # binderhub/binderspawner_mixin.py and helm-chart/binderhub/values.yaml 5 | # and we use a pre-commit hook to automatically update the values in 6 | # values.yaml. 7 | # 8 | # We should remove the embedded code from values.yaml and install the required 9 | # BinderSpawner code in the JupyterHub container. 10 | # 11 | 12 | # For now just check that the two are in sync 13 | import argparse 14 | import difflib 15 | import os 16 | import sys 17 | 18 | from ruamel.yaml import YAML 19 | 20 | yaml = YAML() 21 | yaml.preserve_quotes = True 22 | yaml.indent(mapping=2, sequence=4, offset=2) 23 | 24 | parser = argparse.ArgumentParser(description="Check embedded chart code") 25 | parser.add_argument( 26 | "--update", action="store_true", help="Update binderhub code from values.yaml" 27 | ) 28 | args = parser.parse_args() 29 | 30 | root = os.path.join(os.path.dirname(os.path.realpath(__file__)), os.path.pardir) 31 | binderspawner_mixin_py = os.path.join(root, "binderhub", "binderspawner_mixin.py") 32 | values_yaml = os.path.join(root, "helm-chart", "binderhub", "values.yaml") 33 | 34 | with open(binderspawner_mixin_py) as f: 35 | py_code = f.read() 36 | 37 | 38 | if args.update: 39 | with open(values_yaml) as f: 40 | values = yaml.load(f) 41 | values_code = values["jupyterhub"]["hub"]["extraConfig"]["0-binderspawnermixin"] 42 | if values_code != py_code: 43 | print(f"Generating {values_yaml} from {binderspawner_mixin_py}") 44 | values["jupyterhub"]["hub"]["extraConfig"]["0-binderspawnermixin"] = py_code 45 | with open(values_yaml, "w") as f: 46 | yaml.dump(values, f) 47 | else: 48 | with open(values_yaml) as f: 49 | values = yaml.load(f) 50 | values_code = values["jupyterhub"]["hub"]["extraConfig"]["0-binderspawnermixin"] 51 | 52 | difflines = list( 53 | difflib.context_diff(values_code.splitlines(), py_code.splitlines()) 54 | ) 55 | if difflines: 56 | print("\n".join(difflines)) 57 | print("\n") 58 | print("Values code is not in sync with binderhub/binderspawner_mixin.py") 59 | print( 60 | f"Run `python {sys.argv[0]} --update` to update values.yaml from binderhub/binderspawner_mixin.py" 61 | ) 62 | sys.exit(1) 63 | -------------------------------------------------------------------------------- /ci/common: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Use https://www.shellcheck.net/ to reduce mistakes if you make changes to this file. 3 | 4 | await_jupyterhub() { 5 | kubectl rollout status --watch --timeout 300s deployment/proxy \ 6 | && kubectl rollout status --watch --timeout 300s deployment/hub \ 7 | && ( 8 | if kubectl get deploy/autohttps > /dev/null 2>&1; then 9 | kubectl rollout status --watch --timeout 300s deployment/autohttps 10 | fi 11 | ) 12 | } 13 | 14 | await_binderhub() { 15 | # accepts the release name as a parameter 16 | await_jupyterhub \ 17 | && kubectl rollout status --watch --timeout 300s deployment/binder \ 18 | && ( 19 | if kubectl get "daemonset/${1}-dind" > /dev/null 2>&1; then 20 | kubectl rollout status --watch --timeout 300s "daemonset/${1}-dind" 21 | fi 22 | ) \ 23 | && ( 24 | if kubectl get "daemonset/${1}-image-cleaner" > /dev/null 2>&1; then 25 | kubectl rollout status --watch --timeout 300s "daemonset/${1}-image-cleaner" 26 | fi 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /ci/publish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script publishes the Helm chart to the JupyterHub Helm chart repo and 3 | # pushes associated built docker images to Docker hub using chartpress. 4 | # -------------------------------------------------------------------------- 5 | 6 | # Exit on errors, assert env vars, log commands 7 | set -eux 8 | 9 | PUBLISH_ARGS="--push \ 10 | --builder docker-buildx \ 11 | --platform linux/amd64 --platform linux/arm64 \ 12 | " 13 | 14 | cd helm-chart 15 | # chartpress use git to push to our Helm chart repository, which is the gh-pages 16 | # branch of jupyterhub/helm-chart. We have installed a private SSH key within 17 | # the ~/.ssh folder with permissions to push to jupyterhub/helm-chart. 18 | if [[ $GITHUB_REF != refs/tags/* ]]; then 19 | # Using --extra-message, we help readers of merged PRs to know what version 20 | # they need to bump to in order to make use of the PR. This is enabled by a 21 | # GitHub notificaiton in the PR like "Github Action user pushed a commit to 22 | # jupyterhub/helm-chart that referenced this pull request..." 23 | # 24 | # ref: https://github.com/jupyterhub/chartpress#usage 25 | # 26 | # NOTE: GitHub merge commits contain a PR reference like #123. `sed` looks 27 | # to extract either a PR reference like #123 or fall back to create a 28 | # commit hash reference like @123abcd. Combined with GITHUB_REPOSITORY 29 | # we craft a commit message like jupyterhub/binderhub#123 or 30 | # jupyterhub/binderhub@123abcd which will be understood as a reference 31 | # by GitHub. 32 | PR_OR_HASH=$(git log -1 --pretty=%h-%B | head -n1 | sed 's/^.*\(#[0-9]*\).*/\1/' | sed 's/^\([0-9a-f]*\)-.*/@\1/') 33 | LATEST_COMMIT_TITLE=$(git log -1 --pretty=%B | head -n1) 34 | EXTRA_MESSAGE="${GITHUB_REPOSITORY}${PR_OR_HASH} ${LATEST_COMMIT_TITLE}" 35 | chartpress $PUBLISH_ARGS --extra-message "${EXTRA_MESSAGE}" --publish-chart 36 | chartpress $PUBLISH_ARGS --extra-message "${EXTRA_MESSAGE}" --image-prefix=jupyterhub/k8s- 37 | else 38 | # Setting a tag explicitly enforces a rebuild if this tag had already been 39 | # built and we wanted to override it. 40 | chartpress $PUBLISH_ARGS --tag "${GITHUB_REF:10}" --publish-chart 41 | chartpress $PUBLISH_ARGS --tag "${GITHUB_REF:10}" --image-prefix=jupyterhub/k8s- 42 | fi 43 | 44 | # Let us log the changes chartpress did, it should include replacements for 45 | # fields in values.yaml, such as what tag for various images we are using. 46 | git --no-pager diff --color=always 47 | -------------------------------------------------------------------------------- /ci/refreeze: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -xeuo pipefail 3 | 4 | # Because `pip-compile` resolves `requirements.txt` with the current 5 | # Python for the current platform, it should be run on the same Python 6 | # version and platform as our Dockerfile. 7 | 8 | docker run --rm \ 9 | --env=CUSTOM_COMPILE_COMMAND='Use the "Run workflow" button at https://github.com/jupyterhub/binderhub/actions/workflows/watch-dependencies.yaml' \ 10 | --volume="$PWD:/io" \ 11 | --workdir=/io \ 12 | --user=root \ 13 | python:3.13-bookworm \ 14 | sh -c 'pip install pip-tools==7.* && pip-compile --allow-unsafe --strip-extras --upgrade helm-chart/images/binderhub/requirements.in' 15 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | """top-level pytest config 2 | 3 | options can only be defined here, 4 | not in binderhub/tests/conftest.py 5 | """ 6 | 7 | import nest_asyncio 8 | 9 | 10 | def pytest_addoption(parser): 11 | parser.addoption( 12 | "--helm", 13 | action="store_true", 14 | default=False, 15 | help="Run tests marked with pytest.mark.helm", 16 | ) 17 | 18 | 19 | def pytest_configure(): 20 | nest_asyncio.apply() 21 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4[html5lib] 2 | build 3 | chartpress>=2.1 4 | click 5 | dockerspawner 6 | jsonschema 7 | jupyter-repo2docker>=2021.08.0 8 | jupyter_packaging>=0.10.4,<2 9 | jupyterhub 10 | nest-asyncio 11 | pytest 12 | pytest-asyncio<=0.25.0 13 | pytest-cov 14 | pytest-timeout 15 | pytest_playwright 16 | requests 17 | ruamel.yaml>=0.17.30 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation generated by sphinx-quickstart 2 | # ---------------------------------------------------------------------------- 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) 21 | 22 | 23 | # Manually added commands 24 | # ---------------------------------------------------------------------------- 25 | 26 | # For local development: 27 | # - builds and rebuilds html on changes to source 28 | # - starts a livereload enabled webserver and opens up a browser 29 | devenv: 30 | sphinx-autobuild -b html --open-browser "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) 31 | 32 | # For local development and CI: 33 | # - verifies that links are valid 34 | linkcheck: 35 | $(SPHINXBUILD) -b linkcheck "$(SOURCEDIR)" "$(BUILDDIR)/linkcheck" $(SPHINXOPTS) 36 | @echo 37 | @echo "Link check complete; look for any errors in the above output " \ 38 | "or in $(BUILDDIR)/linkcheck/output.txt." 39 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | if "%1" == "devenv" goto devenv 15 | if "%1" == "linkcheck" goto linkcheck 16 | goto default 17 | 18 | 19 | :default 20 | %SPHINXBUILD% >NUL 2>NUL 21 | if errorlevel 9009 ( 22 | echo. 23 | echo.The 'sphinx-build' command was not found. Open and read README.md! 24 | exit /b 1 25 | ) 26 | %SPHINXBUILD% -M %1 "%SOURCEDIR%" "%BUILDDIR%" %SPHINXOPTS% 27 | goto end 28 | 29 | 30 | :help 31 | %SPHINXBUILD% -M help "%SOURCEDIR%" "%BUILDDIR%" %SPHINXOPTS% 32 | goto end 33 | 34 | 35 | :devenv 36 | sphinx-autobuild >NUL 2>NUL 37 | if errorlevel 9009 ( 38 | echo. 39 | echo.The 'sphinx-autobuild' command was not found. Open and read README.md! 40 | exit /b 1 41 | ) 42 | sphinx-autobuild -b html --open-browser "%SOURCEDIR%" "%BUILDDIR%/html" %SPHINXOPTS% 43 | goto end 44 | 45 | 46 | :linkcheck 47 | %SPHINXBUILD% -b linkcheck "%SOURCEDIR%" "%BUILDDIR%/linkcheck" %SPHINXOPTS% 48 | echo. 49 | echo.Link check complete; look for any errors in the above output 50 | echo.or in "%BUILDDIR%/linkcheck/output.txt". 51 | goto end 52 | 53 | 54 | :end 55 | popd 56 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # sphinx.ext.autodoc and autodoc_traits as configured to be used in conf.py is 2 | # automatically building documentation based on inspection of code in the 3 | # binderhub package, which means we need to its install dependencies to make 4 | # that code loadable for inspection. 5 | -r ../requirements.txt 6 | 7 | # Documentation specific packages 8 | autodoc-traits 9 | myst-parser 10 | pydata-sphinx-theme 11 | sphinx-autobuild 12 | sphinx-copybutton 13 | sphinxext-opengraph 14 | sphinxext-rediraffe 15 | -------------------------------------------------------------------------------- /docs/source/_static/custom.css: -------------------------------------------------------------------------------- 1 | .right-next { 2 | float: right; 3 | max-width: 45%; 4 | overflow: auto; 5 | text-overflow: ellipsis; 6 | white-space: nowrap; 7 | } 8 | 9 | .right-next::after { 10 | content: " »"; 11 | } 12 | 13 | .left-prev { 14 | float: left; 15 | max-width: 45%; 16 | overflow: auto; 17 | text-overflow: ellipsis; 18 | white-space: nowrap; 19 | } 20 | 21 | .left-prev::before { 22 | content: "« "; 23 | } 24 | 25 | .prev-next-bottom { 26 | margin-top: 3em; 27 | } 28 | 29 | .prev-next-top { 30 | margin-bottom: 1em; 31 | } 32 | 33 | /* Federation page CSS */ 34 | .contrib_entry p { 35 | margin: 2px; 36 | text-align: center; 37 | } 38 | 39 | p.contrib_affiliation { 40 | font-size: 0.7em; 41 | font-weight: bold; 42 | } 43 | 44 | .contribs_text { 45 | font-size: 0.8em; 46 | } 47 | 48 | .contrib_entry p.contribs { 49 | float: left; 50 | margin: 0px; 51 | } 52 | 53 | td.contrib_entry { 54 | width: 200px; 55 | height: 200px; 56 | padding: 1em !important; 57 | vertical-align: top; 58 | text-align: center; 59 | border: 1px solid #888 !important; 60 | } 61 | 62 | td.contrib_entry a { 63 | text-decoration: none; 64 | } 65 | 66 | td.contrib_entry img { 67 | width: 100%; 68 | } 69 | 70 | table.contributors { 71 | margin: 0px auto; 72 | } 73 | -------------------------------------------------------------------------------- /docs/source/_static/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/docs/source/_static/images/architecture.png -------------------------------------------------------------------------------- /docs/source/_static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/docs/source/_static/images/favicon.png -------------------------------------------------------------------------------- /docs/source/_static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/docs/source/_static/images/logo.png -------------------------------------------------------------------------------- /docs/source/_templates/navigation.html: -------------------------------------------------------------------------------- 1 | {# Custom template for navigation.html 2 | 3 | alabaster theme does not provide blocks for titles to 4 | be overridden so this custom theme handles title and 5 | toctree for sidebar 6 | #} 7 |

{{ _('Table of Contents') }}

8 | {{ toctree(includehidden=theme_sidebar_includehidden, collapse=theme_sidebar_collapse) }} 9 | {% if theme_extra_nav_links %} 10 |
11 |
    12 | {% for text, uri in theme_extra_nav_links.items() %} 13 |
  • {{ text }}
  • 14 | {% endfor %} 15 |
16 | {% endif %} 17 | -------------------------------------------------------------------------------- /docs/source/authentication.rst: -------------------------------------------------------------------------------- 1 | Enabling Authentication 2 | ======================= 3 | 4 | By default BinderHub runs without authentication and 5 | for each launch it creates a temporary user and starts a server for that user. 6 | 7 | In order to enable authentication for BinderHub by using JupyterHub as an oauth provider, 8 | you need to add the following into ``config.yaml``: 9 | 10 | .. code:: yaml 11 | 12 | config: 13 | BinderHub: 14 | auth_enabled: true 15 | 16 | jupyterhub: 17 | cull: 18 | # don't cull authenticated users (reverts binderhub chart's default) 19 | users: false 20 | hub: 21 | config: 22 | BinderSpawner: 23 | auth_enabled: true 24 | JupyterHub: 25 | redirect_to_server: false 26 | # specify the desired authenticator 27 | authenticator_class: 28 | # use config of your authenticator here 29 | # use the docs at https://zero-to-jupyterhub.readthedocs.io/en/stable/authentication.html 30 | # to get more info about different config options 31 | Authenticator: {} 32 | : {} 33 | services: 34 | binder: 35 | oauth_client_id: service-binderhub 36 | oauth_no_confirm: true 37 | oauth_redirect_uri: "https:///oauth_callback" 38 | loadRoles: 39 | user: 40 | scopes: 41 | - self 42 | - "access:services!service=binder" 43 | 44 | singleuser: 45 | # make notebook servers aware of hub (reverts binderhub chart's default to z2jh chart's default) 46 | cmd: jupyterhub-singleuser 47 | 48 | If the configuration above was entered correctly, once you upgrade your 49 | BinderHub Helm Chart with ``helm upgrade...``, users that arrive at your 50 | BinderHub URL will be directed to a login page. Once they enter their 51 | credentials, they'll be taken to the typical BinderHub landing page. 52 | 53 | .. note:: 54 | 55 | If users *don't* go to a BinderHub landing page after they log-in, 56 | then the configuration above is probably incorrect. Double-check that 57 | the BinderHub configuration (and the JupyterHub authentication configuration) 58 | look good. 59 | .. note:: 60 | For the authentication config in ``jupyterhub.hub.config``, 61 | you should use config of your authenticator. For more information you can check 62 | `the Authentication guide 63 | `_. 64 | 65 | .. warning:: 66 | ``jupyterhub-singleuser`` requires ``JupyterHub`` to be installed in user server images. 67 | Therefore ensure that you use at least ``jupyter/repo2docker:ccce3fe`` image 68 | to build user images. Because ``repo2docker`` installs ``JupyterHub`` by default after that. 69 | 70 | Authentication with named servers 71 | --------------------------------- 72 | 73 | With above configuration Binderhub limits each authenticated user to start one server at a time. 74 | When a user already has a running server, BinderHub displays an error message. 75 | 76 | If you want to have users be able to launch multiple servers at the same time, 77 | you have to enable named servers on JupyterHub: 78 | 79 | .. code:: yaml 80 | 81 | jupyterhub: 82 | hub: 83 | allowNamedServers: true 84 | # change this value as you wish, 85 | # or set to 0 if you don't want to have any limit 86 | namedServerLimitPerUser: 5 87 | 88 | .. note:: 89 | BinderHub assigns a unique name to each server with max 40 characters. 90 | -------------------------------------------------------------------------------- /docs/source/cors.rst: -------------------------------------------------------------------------------- 1 | Enabling CORS 2 | ============= 3 | 4 | Cross-Origin Resource Sharing (CORS) is a mechanism that gives a 5 | web application running at one origin, access to resources from a 6 | different origin. For security reasons, browsers restrict these 7 | "cross-origin" requests by default. 8 | 9 | In the context of a BinderHub deployment, CORS is relevant when you 10 | wish to leverage binder as a computing backend for a web application 11 | hosted at some other domain. For example, the amazing libraries 12 | `Juniper `_ and 13 | `Thebe `_ leverage binder as 14 | a computing backend to facilitate live, interactive coding, directly 15 | within a static HTML webpage. For this functionality, CORS must be 16 | enabled. 17 | 18 | Adjusting BinderHub config to enable CORS 19 | ----------------------------------------- 20 | 21 | As mentioned above, for security reasons, CORS is not enabled by 22 | default for BinderHub deployments. To enable CORS we need to add 23 | additional HTTP headers to allow our BinderHub deployment to be 24 | accessed from a different origin. This is as simple as adding the 25 | following to your ``config.yaml``: 26 | 27 | .. code:: yaml 28 | 29 | config: 30 | BinderHub: 31 | cors_allow_origin: '*' 32 | 33 | jupyterhub: 34 | hub: 35 | config: 36 | BinderSpawner: 37 | cors_allow_origin: '*' 38 | 39 | For example, if you're following on from the previous section 40 | :doc:`../https`, your ``config.yaml`` might look like this: 41 | 42 | .. code:: yaml 43 | 44 | config: 45 | BinderHub: 46 | hub_url: https:// # e.g. https://hub.binder.example.com 47 | cors_allow_origin: '*' 48 | 49 | jupyterhub: 50 | hub: 51 | config: 52 | BinderSpawner: 53 | cors_allow_origin: '*' 54 | ingress: 55 | enabled: true 56 | hosts: 57 | - # e.g. hub.binder.example.com 58 | annotations: 59 | kubernetes.io/ingress.class: nginx 60 | kubernetes.io/tls-acme: "true" 61 | cert-manager.io/issuer: letsencrypt-production 62 | https: 63 | enabled: true 64 | type: nginx 65 | tls: 66 | - secretName: -tls # e.g. hub-binder-example-com-tls 67 | hosts: 68 | - # e.g. hub.binder.example.com 69 | 70 | ingress: 71 | enabled: true 72 | hosts: 73 | - # e.g. binder.example.com 74 | annotations: 75 | kubernetes.io/ingress.class: nginx 76 | kubernetes.io/tls-acme: "true" 77 | cert-manager.io/issuer: letsencrypt-production 78 | https: 79 | enabled: true 80 | type: nginx 81 | tls: 82 | - secretName: -tls # e.g. binder-example-com-tls 83 | hosts: 84 | - # e.g. binder.example.com 85 | 86 | Once you've adjusted ``config.yaml`` to enable CORS, apply your changes 87 | with:: 88 | 89 | helm upgrade jupyterhub/binderhub --version= -f secret.yaml -f config.yaml 90 | 91 | It may take ~10 minutes for the changes to take effect. 92 | -------------------------------------------------------------------------------- /docs/source/customization/index.rst: -------------------------------------------------------------------------------- 1 | Customize and Maintain 2 | ====================== 3 | 4 | These pages cover common use-cases and needs when deploying your own BinderHub. 5 | This includes configuring and customizing your BinderHub deployment, as well as 6 | best-practices in debugging issues. 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | :caption: Customization and deployment 11 | 12 | ../customizing 13 | ../debug 14 | ../authentication 15 | ../https 16 | ../cors 17 | -------------------------------------------------------------------------------- /docs/source/developer/index.rst: -------------------------------------------------------------------------------- 1 | Technical Reference 2 | =================== 3 | 4 | A more detailed overview of the BinderHub design, architecture, and functionality. 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Developer and architecture docs 9 | 10 | ../overview 11 | ../eventlogging 12 | ../api 13 | repoproviders 14 | ../reference/ref-index.rst 15 | -------------------------------------------------------------------------------- /docs/source/eventlogging.rst: -------------------------------------------------------------------------------- 1 | .. _eventlogging: 2 | 3 | ============= 4 | Event Logging 5 | ============= 6 | 7 | Events are discrete & structured items emitted by 8 | BinderHub when specific events happen. For example, 9 | the ``binderhub.jupyter.org/launch`` event is emitted 10 | whenever a Launch succeeds. 11 | 12 | These events may be sent to a *sink* via handlers 13 | from the python ``logging`` module. 14 | 15 | Events vs Metrics 16 | ================= 17 | 18 | BinderHub also exposes `prometheus `_ 19 | metrics. These are pre-aggregated, and extremely limited in 20 | scope. They can efficiently answer questions like 'how many launches 21 | happened in the last hour?' but not questions like 'how 22 | many times was this repo launched in the last 6 months?'. 23 | Events are discrete and can be aggregated in many ways 24 | during analysis. Metrics are aggregated at source, and this 25 | limits what can be done with them during analysis. Metrics 26 | are mostly operational, while events are for analytics. 27 | 28 | What events to emit? 29 | ==================== 30 | 31 | Since events have a lot more information than metrics do, 32 | we should be careful about what events we emit. In general, 33 | we should pose an **explicit question** that events can answer. 34 | 35 | For example, to answer the question *How many times has my 36 | GitHub repo been launched in the last 6 months?*, we would need 37 | to emit an event every time a launch succeeds. To answer the 38 | question *how long did users spend on my repo?*, we would need 39 | to emit an event every time a user notebook is killed, along 40 | with the lifetime length of the notebook. 41 | 42 | `Wikimedia's EventLogging Guidelines `_ 43 | contain a lot of useful info on how to approach adding more events. 44 | 45 | BinderHub Events 46 | ================ 47 | 48 | Launch event 49 | ------------ 50 | 51 | This event is emitted whenever a new repo is launched. 52 | 53 | Schemas: 54 | 55 | - `version 1 `_ 56 | - `version 2 `_ 57 | - `version 3 `_ 58 | - `version 4 `_ 59 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | BinderHub 3 | ========= 4 | 5 | .. image:: https://badges.gitter.im/jupyterhub/binder.svg 6 | :alt: Join the chat at https://gitter.im/jupyterhub/binder 7 | :target: https://gitter.im/jupyterhub/binder 8 | 9 | .. image:: https://img.shields.io/badge/help_forum-discourse-blue.svg 10 | :alt: Join our community Discourse page at https://discourse.jupyter.org 11 | :target: https://discourse.jupyter.org/c/binder/binderhub 12 | 13 | 14 | BinderHub is a kubernetes-based cloud service that allows users to share 15 | reproducible interactive computing environments from code repositories. It is 16 | the primary technology behind `mybinder.org `_. 17 | 18 | This guide assists you, an administrator, through the process of setting up 19 | your BinderHub deployment. 20 | 21 | .. tip:: For information about **using a BinderHub**, see `the binder user documentation `_. 22 | 23 | To get started creating your own BinderHub, start with :ref:`zero-to-binderhub`. 24 | 25 | Zero to BinderHub 26 | ================= 27 | 28 | A guide to help you create your own BinderHub from scratch. 29 | 30 | .. toctree:: 31 | :maxdepth: 2 32 | 33 | zero-to-binderhub/index 34 | 35 | Customization and deployment information 36 | ======================================== 37 | 38 | Information on how to customize your BinderHub as well as explore what others 39 | in the community have done. 40 | 41 | .. toctree:: 42 | :maxdepth: 2 43 | 44 | customization/index 45 | 46 | BinderHub Developer and Architecture Documentation 47 | ================================================== 48 | 49 | A more detailed overview of the BinderHub design, architecture, and functionality. 50 | 51 | .. toctree:: 52 | :maxdepth: 2 53 | :caption: Developer and architecture docs 54 | 55 | developer/index 56 | 57 | Contributing to BinderHub 58 | ========================= 59 | 60 | The BinderHub community includes members of organizations deploying their own BinderHubs, 61 | as well as members of the broader Jupyter and Binder communities. 62 | 63 | This section contains a collection of resources for and about the BinderHub community. 64 | 65 | .. toctree:: 66 | :maxdepth: 2 67 | 68 | contribute 69 | -------------------------------------------------------------------------------- /docs/source/overview.rst: -------------------------------------------------------------------------------- 1 | .. _diagram: 2 | 3 | The BinderHub Architecture 4 | ========================== 5 | 6 | This page provides a high-level overview of the technical pieces that make 7 | up a BinderHub deployment. 8 | 9 | Tools used by BinderHub 10 | ----------------------- 11 | 12 | BinderHub connects several services together to provide on-the-fly creation 13 | and registry of Docker images. It utilizes the following tools: 14 | 15 | - A **cloud provider** such Google Cloud, Microsoft Azure, Amazon EC2, and 16 | others 17 | - **Kubernetes** to manage resources on the cloud 18 | - **Helm** to configure and control Kubernetes 19 | - **Docker** to use containers that standardize computing environments 20 | - A **BinderHub UI** that users can access to specify Git repos they want 21 | built 22 | - **repo2docker** to generate Docker images using the URL of a Git repository 23 | - A **Docker registry** (such as gcr.io) that hosts container images 24 | - **JupyterHub** to deploy temporary containers for users 25 | 26 | What happens when a user clicks a Binder link? 27 | ---------------------------------------------- 28 | 29 | After a user clicks a Binder link, the following chain of events happens: 30 | 31 | 1. BinderHub resolves the link to the repository. 32 | 2. BinderHub determines whether a Docker image already exists for the repository at the latest 33 | ``ref`` (git commit hash, branch, or tag). 34 | 3. **If the image doesn't exist**, BinderHub creates a ``build`` pod that uses 35 | `repo2docker `_ to do the following: 36 | 37 | - Fetch the repository associated with the link 38 | - Build a Docker container image containing the environment specified in 39 | `configuration files `_ 40 | in the repository. 41 | - Push that image to a Docker registry, and send the registry information 42 | to the BinderHub for future reference. 43 | 4. BinderHub sends the Docker image registry to **JupyterHub**. 44 | 5. JupyterHub creates a Kubernetes pod for the user that serves the built Docker image 45 | for the repository. 46 | 6. JupyterHub monitors the user's pod for activity, and destroys it after a short period of 47 | inactivity. 48 | 49 | A diagram of the BinderHub architecture 50 | --------------------------------------- 51 | 52 | 53 | .. This image was generated at the following URL: https://docs.google.com/presentation/d/1t5W4Rnez6xBRz4YxCxWYAx8t4KRfUosbCjS4Z1or7rM/edit#slide=id.g25dbc82125_0_53 54 | 55 | .. figure:: _static/images/architecture.png 56 | 57 | Here is a high-level overview of the components that make up BinderHub. 58 | -------------------------------------------------------------------------------- /docs/source/reference/app.rst: -------------------------------------------------------------------------------- 1 | app 2 | === 3 | 4 | 5 | Module: :mod:`binderhub.app` 6 | ---------------------------- 7 | 8 | .. automodule:: binderhub.app 9 | 10 | .. currentmodule:: binderhub.app 11 | 12 | :class:`BinderHub` 13 | ------------------ 14 | 15 | .. autoconfigurable:: BinderHub 16 | :members: 17 | -------------------------------------------------------------------------------- /docs/source/reference/build.rst: -------------------------------------------------------------------------------- 1 | build 2 | ===== 3 | 4 | 5 | Module: :mod:`binderhub.build` 6 | ------------------------------ 7 | 8 | .. automodule:: binderhub.build 9 | 10 | .. currentmodule:: binderhub.build 11 | 12 | 13 | :class:`Build` 14 | -------------- 15 | 16 | .. autoclass:: Build 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/source/reference/builder.rst: -------------------------------------------------------------------------------- 1 | builder 2 | ======= 3 | 4 | 5 | Module: :mod:`binderhub.builder` 6 | -------------------------------- 7 | 8 | .. automodule:: binderhub.builder 9 | 10 | .. currentmodule:: binderhub.builder 11 | 12 | :class:`BuildHandler` 13 | --------------------- 14 | 15 | .. autoclass:: BuildHandler 16 | :members: 17 | -------------------------------------------------------------------------------- /docs/source/reference/launcher.rst: -------------------------------------------------------------------------------- 1 | launcher 2 | ======== 3 | 4 | 5 | Module: :mod:`binderhub.launcher` 6 | --------------------------------- 7 | 8 | .. automodule:: binderhub.launcher 9 | 10 | .. currentmodule:: binderhub.launcher 11 | 12 | :class:`Launcher` 13 | ----------------- 14 | 15 | .. autoconfigurable:: Launcher 16 | :members: 17 | -------------------------------------------------------------------------------- /docs/source/reference/main.rst: -------------------------------------------------------------------------------- 1 | main 2 | ==== 3 | 4 | 5 | Module: :mod:`binderhub.main` 6 | ----------------------------- 7 | 8 | .. automodule:: binderhub.main 9 | 10 | .. currentmodule:: binderhub.main 11 | 12 | :class:`MainHandler` 13 | -------------------- 14 | 15 | .. autoclass:: MainHandler 16 | :members: 17 | 18 | :class:`ParameterizedMainHandler` 19 | --------------------------------- 20 | 21 | .. autoclass:: ParameterizedMainHandler 22 | :members: 23 | 24 | :class:`LegacyRedirectHandler` 25 | ------------------------------ 26 | 27 | .. autoclass:: LegacyRedirectHandler 28 | :members: 29 | -------------------------------------------------------------------------------- /docs/source/reference/ref-index.rst: -------------------------------------------------------------------------------- 1 | Configuration and Source Code Reference 2 | ======================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | app 8 | build 9 | builder 10 | launcher 11 | main 12 | registry 13 | repoproviders 14 | -------------------------------------------------------------------------------- /docs/source/reference/registry.rst: -------------------------------------------------------------------------------- 1 | registry 2 | ======== 3 | 4 | binderhub.repoproviders 5 | 6 | 7 | Module: :mod:`binderhub.registry` 8 | --------------------------------- 9 | 10 | .. automodule:: binderhub.registry 11 | 12 | .. currentmodule:: binderhub.registry 13 | 14 | 15 | :class:`DockerRegistry` 16 | ----------------------- 17 | 18 | .. autoclass:: DockerRegistry 19 | :members: 20 | -------------------------------------------------------------------------------- /docs/source/reference/repoproviders.rst: -------------------------------------------------------------------------------- 1 | .. _api-repoproviders: 2 | 3 | repoproviders 4 | ============= 5 | 6 | Module: :mod:`binderhub.repoproviders` 7 | -------------------------------------- 8 | 9 | .. automodule:: binderhub.repoproviders 10 | 11 | .. currentmodule:: binderhub.repoproviders 12 | 13 | :class:`RepoProvider` 14 | --------------------- 15 | 16 | .. autoconfigurable:: RepoProvider 17 | :members: 18 | 19 | 20 | :class:`GitHubRepoProvider` 21 | --------------------------- 22 | 23 | .. autoconfigurable:: GitHubRepoProvider 24 | :members: 25 | 26 | 27 | :class:`GitLabRepoProvider` 28 | --------------------------- 29 | 30 | .. autoconfigurable:: GitLabRepoProvider 31 | :members: 32 | 33 | 34 | :class:`GistRepoProvider` 35 | --------------------------- 36 | 37 | .. autoconfigurable:: GistRepoProvider 38 | :members: 39 | 40 | 41 | :class:`ZenodoProvider` 42 | --------------------------- 43 | 44 | .. autoconfigurable:: ZenodoProvider 45 | :members: 46 | 47 | 48 | :class:`FigshareProvider` 49 | --------------------------- 50 | 51 | .. autoconfigurable:: FigshareProvider 52 | :members: 53 | 54 | 55 | :class:`HydroshareProvider` 56 | --------------------------- 57 | 58 | .. autoconfigurable:: HydroshareProvider 59 | :members: 60 | 61 | 62 | :class:`DataverseProvider` 63 | --------------------------- 64 | 65 | .. autoconfigurable:: DataverseProvider 66 | :members: 67 | 68 | :class:`CKANProvider` 69 | --------------------------- 70 | 71 | .. autoconfigurable:: CKANProvider 72 | :members: 73 | 74 | :class:`GitRepoProvider` 75 | --------------------------- 76 | 77 | .. autoconfigurable:: GitRepoProvider 78 | :members: 79 | -------------------------------------------------------------------------------- /docs/source/zero-to-binderhub/index.rst: -------------------------------------------------------------------------------- 1 | .. _zero-to-binderhub: 2 | 3 | ================= 4 | Zero to BinderHub 5 | ================= 6 | 7 | A guide to help you create your own BinderHub from scratch. Each section below 8 | covers one of the aspects of setting up a BinderHub for your users. 9 | 10 | .. note:: 11 | 12 | BinderHub uses a JupyterHub running on Kubernetes for most of its functionality. 13 | This guide assumes you have experience with setting up a JupyterHub on Kubernetes. 14 | We recommend you follow the `Zero to JupyterHub Guide `_ 15 | to familiarize yourself with JupyterHub on Kubernetes before attempting to 16 | setup a BinderHub. 17 | 18 | .. toctree:: 19 | :maxdepth: 2 20 | :numbered: 21 | :caption: Zero to BinderHub 22 | 23 | setup-prerequisites 24 | setup-registry 25 | setup-binderhub 26 | turn-off 27 | -------------------------------------------------------------------------------- /docs/source/zero-to-binderhub/ovh/generate_id_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/docs/source/zero-to-binderhub/ovh/generate_id_details.png -------------------------------------------------------------------------------- /docs/source/zero-to-binderhub/ovh/new_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/docs/source/zero-to-binderhub/ovh/new_project.png -------------------------------------------------------------------------------- /docs/source/zero-to-binderhub/private-gitlab-repo-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/docs/source/zero-to-binderhub/private-gitlab-repo-token.png -------------------------------------------------------------------------------- /docs/source/zero-to-binderhub/private-repo-token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterhub/binderhub/dcc8ca9a6bad6ef9e6c64a5aba669904a34b9292/docs/source/zero-to-binderhub/private-repo-token.png -------------------------------------------------------------------------------- /docs/source/zero-to-binderhub/setup-prerequisites.rst: -------------------------------------------------------------------------------- 1 | .. _create-cluster: 2 | 3 | Set up the prerequisites 4 | ======================== 5 | 6 | BinderHub is built to run in a `Kubernetes cluster `_. It 7 | relies on JupyterHub to launch and manage user servers, as well as a docker 8 | registry to cache docker images it builds. 9 | 10 | To deploy your own BinderHub, you'll first need to set up a Kubernetes cluster. 11 | The following instructions will assist you in doing so. 12 | 13 | Setting up a Kubernetes cluster 14 | ------------------------------- 15 | 16 | First, deploy a Kubernetes cluster by following the `instructions in the Zero to 17 | JupyterHub guide 18 | `_. 19 | When you're done, move on to the next section. 20 | 21 | Local Kubernetes cluster 22 | ~~~~~~~~~~~~~~~~~~~~~~~~ 23 | 24 | If you are using a local Kubernetes cluster on a single computer 25 | for learning and development, you might need to setup a ingress controller 26 | such as `Ingress NGINX Controller `_ 27 | and run JupyterHub and BinderHub services as ``ClusterIP`` instead of ``LoadBalancer``. 28 | This is due some limitations with local Kubernetes cluster. 29 | 30 | Installing Helm 31 | --------------- 32 | 33 | `Helm `_, the package manager for Kubernetes, is a useful tool 34 | for: installing, upgrading and managing applications on a Kubernetes cluster. 35 | Helm packages are called *charts*. We will be installing and managing JupyterHub 36 | on our Kubernetes cluster using a Helm chart. 37 | 38 | A Helm *chart* is mostly Helm *templates* and default *values* that are used to 39 | render the templates into valid k8s resources. Each installation of a chart is 40 | called a *release*, and each version of the release is called a *revision*. 41 | 42 | Several `methods to install Helm 43 | `_ exist, the simplest 44 | way to install Helm is to run Helm's installer script in a terminal. 45 | 46 | .. code:: bash 47 | 48 | curl -sf https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 49 | 50 | Verifying the setup 51 | ~~~~~~~~~~~~~~~~~~~ 52 | 53 | Verify that you have installed helm, kubectl, and have an ability to communicate 54 | with your Kubernetes cluster. 55 | 56 | .. code:: bash 57 | 58 | helm version 59 | 60 | Which will output something similar to: 61 | 62 | .. code-block:: bash 63 | 64 | version.BuildInfo{Version:"v3.4.0", GitCommit:"7090a89efc8a18f3d8178bf47d2462450349a004", GitTreeState:"clean", GoVersion:"go1.14.10"} 65 | 66 | Then check your kubectl version: 67 | 68 | .. code-block:: bash 69 | 70 | kubectl version 71 | 72 | Which will output something similar to: 73 | 74 | .. code-block:: bash 75 | 76 | Client Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.3", GitCommit:"1e11e4a2108024935ecfcb2912226cedeafd99df", GitTreeState:"clean", BuildDate:"2020-10-14T12:50:19Z", GoVersion:"go1.15.2", Compiler:"gc", Platform:"linux/amd64"} 77 | Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.2", GitCommit:"f5743093fd1c663cb0cbc89748f730662345d44d", GitTreeState:"clean", BuildDate:"2020-09-16T13:32:58Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"} 78 | 79 | Now that you've installed Kubernetes and Helm, it's time to :ref:`setup-registry`. 80 | -------------------------------------------------------------------------------- /docs/source/zero-to-binderhub/turn-off.rst: -------------------------------------------------------------------------------- 1 | Tear down your Binder deployment 2 | ================================ 3 | 4 | Deconstructing a Binder deployment can be a little bit confusing because 5 | users may have caused new cloud containers to be created. It is important 6 | to remember to delete each of these containers or else they will continue 7 | to exist (and cost money!). 8 | 9 | Contracting the size of your cluster 10 | ------------------------------------ 11 | 12 | If you would like to shrink the size of your cluster, refer to the 13 | `Expanding and contracting the size of your cluster `_ 14 | section of the `Zero to JupyterHub`_ documentation. Resizing the cluster to 15 | zero nodes could be used if you wish to temporarily reduce the cluster (and 16 | save costs) without deleting the cluster. 17 | 18 | Deleting the cluster 19 | -------------------- 20 | 21 | To delete a Binder cluster, follow the instructions in the 22 | `Turning Off JupyterHub and Computational Resources `_ 23 | section of the `Zero to JupyterHub`_ documentation. 24 | 25 | .. important:: 26 | 27 | Double check your cloud provider account to make sure all resources have been 28 | deleted as expected. Double checking is a good practice and will help 29 | prevent unwanted charges. 30 | 31 | .. _Zero to JupyterHub: https://zero-to-jupyterhub.readthedocs.io 32 | -------------------------------------------------------------------------------- /examples/appendix/README.md: -------------------------------------------------------------------------------- 1 | This example shows how to use 2 | [`appendix`](https://binderhub.readthedocs.io/en/latest/reference/app.html?highlight=c.BinderHub.appendix%20#binderhub.app.BinderHub) 3 | feature of BinderHub. 4 | 5 | Appendix consists of Docker commands which are passed to repo2docker and 6 | executed at the end of each build process. 7 | 8 | ## What does this example do? 9 | 10 | In this example `appendix` is used to customize the Notebook UI: 11 | 12 | 1. Instead of standard notebook login page with form, 13 | display an informative page, e.g. how to launch a new binder. 14 | This is very useful when people share 15 | pod urls instead of binder launch urls. 16 | 17 | 2. Remove logout button 18 | 19 | 3. Add binder buttons next to `Quit button`: 20 | 21 | - `Go to repo`: opens the source repo url in new tab 22 | - `Copy binder link`: copies the binder launch link into clipboard 23 | - `Copy session link`: copies the binder session link into clipboard. 24 | When this link is shared with another user, that user will reach to 25 | the same binder session. 26 | It will not start a new launch. 27 | 28 | ## How does the example work? 29 | 30 | To run the example you have to add the appendix into your BinderHub configuration as shown in 31 | [binderhub_config.py](/examples/appendix/binderhub_config.py). These commands are executed every time when 32 | there is a new build and it does: 33 | 34 | - set environment variables for binder and repo urls 35 | - download this appendix folder 36 | - run [run-appendix](/examples/appendix/run-appendix) script 37 | 38 | `run-appendix` mainly does 2 things: 39 | 40 | 1. Copy `templates` into `/etc/jupyter/binder_templates` 41 | and update notebook app configuration to append this path into `extra_template_paths`. 42 | So when notebook app starts, it uses customized templates. 43 | 44 | 2. Inject Javascript code into `~/.jupyter/custom/custom.js`. This is 45 | executed when the notebook app starts and it adds the buttons. 46 | -------------------------------------------------------------------------------- /examples/appendix/binderhub_config.py: -------------------------------------------------------------------------------- 1 | c.BinderHub.appendix = """ 2 | USER root 3 | ENV BINDER_URL={binder_url} 4 | ENV REPO_URL={repo_url} 5 | RUN cd /tmp \ 6 | && wget -q https://github.com/jupyterhub/binderhub/archive/main.tar.gz -O binderhub.tar.gz \ 7 | && tar --wildcards -xzf binderhub.tar.gz --strip 2 */examples/appendix\ 8 | && ./appendix/run-appendix \ 9 | && rm -rf binderhub.tar.gz appendix 10 | USER $NB_USER 11 | """ 12 | -------------------------------------------------------------------------------- /examples/appendix/extra_notebook_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | c.NotebookApp.extra_template_paths.append("/etc/jupyter/binder_templates") 4 | c.NotebookApp.jinja_template_vars.update( 5 | {"binder_url": os.environ.get("BINDER_URL", "https://mybinder.org")} 6 | ) 7 | -------------------------------------------------------------------------------- /examples/appendix/run-appendix: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | set -x 4 | appendix_dir="$(dirname "$0")" 5 | mkdir -p /etc/jupyter 6 | # add and register custom templates and config 7 | cp -r "${appendix_dir}/templates" /etc/jupyter/binder_templates 8 | cat "${appendix_dir}/extra_notebook_config.py" >> /etc/jupyter/jupyter_notebook_config.py 9 | # ensure /etc/jupyter has read+listdir permissions for all 10 | chmod -R a+rX /etc/jupyter 11 | 12 | # add custom.js. 13 | # it is executed when the notebook app starts and adds binder buttons next to Quit button in the UI. 14 | sed -i 's|{binder_url}|'"$BINDER_URL"'|g' "${appendix_dir}/static/custom.js" 15 | sed -i 's|{repo_url}|'"$REPO_URL"'|g' "${appendix_dir}/static/custom.js" 16 | mkdir -p ~/.jupyter/custom 17 | cat "${appendix_dir}/static/custom.js" >> ~/.jupyter/custom/custom.js 18 | chown -R $NB_USER:$NB_USER $HOME/.jupyter 19 | -------------------------------------------------------------------------------- /examples/appendix/static/custom.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | function copy_link_into_clipboard(b) { 3 | var $temp = $(""); 4 | $(b).parent().append($temp); 5 | $temp.val($(b).data("url")).select(); 6 | document.execCommand("copy"); 7 | $temp.remove(); 8 | } 9 | 10 | function add_binder_buttons() { 11 | var copy_button = 12 | '"; 19 | 20 | var link_button = 21 | '' + 26 | " Go to {name}"; 27 | 28 | var s = $(""); 29 | s.append( 30 | link_button.replace(/{name}/g, "repo").replace("{url}", "{repo_url}"), 31 | ); 32 | s.append( 33 | copy_button.replace(/{name}/g, "binder").replace("{url}", "{binder_url}"), 34 | ); 35 | if ($("#ipython_notebook").length && $("#ipython_notebook>a").length) { 36 | s.append( 37 | copy_button 38 | .replace(/{name}/g, "session") 39 | .replace( 40 | "{url}", 41 | window.location.origin.concat($("#ipython_notebook>a").attr("href")), 42 | ), 43 | ); 44 | } 45 | // add buttons at the end of header-container 46 | $("#header-container").append(s); 47 | } 48 | 49 | add_binder_buttons(); 50 | -------------------------------------------------------------------------------- /examples/appendix/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/login.html" %} 2 | {% block site %} 3 | 4 |
5 |

Binder inaccessible

6 |

7 | You can get a new Binder for this repo by clicking here. 8 |

9 |

10 | The shareable URL for this repo is: {{binder_url}} 11 |

12 | 13 |

Is this a Binder that you created?

14 |

15 | If so, your authentication cookie for this Binder has been deleted or expired. 16 | You can launch a new Binder for this repo by clicking here. 17 |

18 | 19 |

Did someone give you this Binder link?

20 |

21 | If so, the link is outdated or incorrect. 22 | Recheck the link for typos or ask the person who gave you the link for an updated link. 23 | A shareable Binder link should look like {{binder_url}}. 24 |

25 | {% endblock site %} 26 | -------------------------------------------------------------------------------- /examples/appendix/templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/page.html" %} 2 | {% block login_widget %}{% endblock %} 3 | -------------------------------------------------------------------------------- /examples/binder-api.py: -------------------------------------------------------------------------------- 1 | """Launching a binder via API 2 | 3 | The binder build API yields a sequence of messages via event-stream. 4 | This example demonstrates how to consume events from the stream 5 | and redirect to the URL when it is ready. 6 | 7 | When the image is ready, your browser will open at the desired URL. 8 | """ 9 | 10 | import argparse 11 | import json 12 | import sys 13 | import webbrowser 14 | 15 | import requests 16 | 17 | 18 | def build_binder(repo, ref, *, binder_url="https://mybinder.org", build_only): 19 | """Launch a binder 20 | 21 | Yields Binder's event-stream events (dicts) 22 | """ 23 | print(f"Building binder for {repo}@{ref}") 24 | url = binder_url + f"/build/gh/{repo}/{ref}" 25 | params = {} 26 | if build_only: 27 | params = {"build_only": "true"} 28 | 29 | r = requests.get(url, stream=True, params=params) 30 | r.raise_for_status() 31 | for line in r.iter_lines(): 32 | line = line.decode("utf8", "replace") 33 | if line.startswith("data:"): 34 | yield json.loads(line.split(":", 1)[1]) 35 | 36 | 37 | if __name__ == "__main__": 38 | parser = argparse.ArgumentParser(description=__doc__) 39 | parser.add_argument("repo", type=str, help="The GitHub repo to build") 40 | parser.add_argument("--ref", default="HEAD", help="The ref of the repo to build") 41 | parser.add_argument( 42 | "--build-only", 43 | action="store_true", 44 | help="When passed, the image will not be launched after build", 45 | ) 46 | file_or_url = parser.add_mutually_exclusive_group() 47 | file_or_url.add_argument("--filepath", type=str, help="The file to open, if any.") 48 | file_or_url.add_argument("--urlpath", type=str, help="The url to open, if any.") 49 | parser.add_argument( 50 | "--binder", 51 | default="https://mybinder.org", 52 | help=""" 53 | The URL of the binder instance to use. 54 | Use `http://localhost:8585` if you are doing local testing. 55 | """, 56 | ) 57 | opts = parser.parse_args() 58 | 59 | for evt in build_binder( 60 | opts.repo, ref=opts.ref, binder_url=opts.binder, build_only=opts.build_only 61 | ): 62 | if "message" in evt: 63 | print( 64 | "[{phase}] {message}".format( 65 | phase=evt.get("phase", ""), 66 | message=evt["message"].rstrip(), 67 | ) 68 | ) 69 | if evt.get("phase") == "ready": 70 | if opts.build_only: 71 | break 72 | elif opts.filepath: 73 | url = "{url}notebooks/{filepath}?token={token}".format( 74 | **evt, filepath=opts.filepath 75 | ) 76 | elif opts.urlpath: 77 | url = "{url}{urlpath}?token={token}".format(**evt, urlpath=opts.urlpath) 78 | else: 79 | url = "{url}?token={token}".format(**evt) 80 | print(f"Opening {url}") 81 | webbrowser.open(url) 82 | break 83 | else: 84 | sys.exit("binder never became ready") 85 | -------------------------------------------------------------------------------- /helm-chart/.gitignore: -------------------------------------------------------------------------------- 1 | binderhub/values.schema.json 2 | 3 | ### macOS ### 4 | *.DS_Store 5 | .AppleDouble 6 | .LSOverride 7 | 8 | # Icon must end with two \r 9 | Icon 10 | # Thumbnails 11 | ._* 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | # Directories potentially created on remote AFP share 21 | .AppleDB 22 | .AppleDesktop 23 | Network Trash Folder 24 | Temporary Items 25 | .apdisk 26 | 27 | ### Vim ### 28 | # swap 29 | [._]*.s[a-w][a-z] 30 | [._]s[a-w][a-z] 31 | # session 32 | Session.vim 33 | # temporary 34 | .netrwhist 35 | *~ 36 | # auto-generated tag files 37 | tags 38 | 39 | # GCloud Credentials 40 | data8-travis-creds.json 41 | 42 | #### Python .gitignore from https://github.com/github/gitignore/blob/master/Python.gitignore 43 | #### 44 | # Byte-compiled / optimized / DLL files 45 | __pycache__/ 46 | *.py[cod] 47 | *$py.class 48 | 49 | # C extensions 50 | *.so 51 | 52 | # Distribution / packaging 53 | .Python 54 | env/ 55 | build/ 56 | develop-eggs/ 57 | dist/ 58 | downloads/ 59 | eggs/ 60 | .eggs/ 61 | lib/ 62 | lib64/ 63 | parts/ 64 | sdist/ 65 | var/ 66 | wheels/ 67 | *.egg-info/ 68 | .installed.cfg 69 | *.egg 70 | 71 | # PyInstaller 72 | # Usually these files are written by a python script from a template 73 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 74 | *.manifest 75 | *.spec 76 | 77 | # Installer logs 78 | pip-log.txt 79 | pip-delete-this-directory.txt 80 | 81 | # Unit test / coverage reports 82 | htmlcov/ 83 | .tox/ 84 | .coverage 85 | .coverage.* 86 | .cache 87 | nosetests.xml 88 | coverage.xml 89 | *,cover 90 | .hypothesis/ 91 | 92 | # Translations 93 | *.mo 94 | *.pot 95 | 96 | # Django stuff: 97 | *.log 98 | local_settings.py 99 | 100 | # Flask stuff: 101 | instance/ 102 | .webassets-cache 103 | 104 | # Scrapy stuff: 105 | .scrapy 106 | 107 | # Sphinx documentation 108 | docs/_build/ 109 | 110 | # PyBuilder 111 | target/ 112 | 113 | # Jupyter Notebook 114 | .ipynb_checkpoints 115 | 116 | # pyenv 117 | .python-version 118 | 119 | # celery beat schedule file 120 | celerybeat-schedule 121 | 122 | # dotenv 123 | .env 124 | 125 | # virtualenv 126 | .venv 127 | venv/ 128 | ENV/ 129 | 130 | # Spyder project settings 131 | .spyderproject 132 | 133 | # Rope project settings 134 | .ropeproject 135 | -------------------------------------------------------------------------------- /helm-chart/README.md: -------------------------------------------------------------------------------- 1 | # BinderHub Helm Chart 2 | 3 | A [helm][] [chart][] for deploying [BinderHub] instances on [Kubernetes]. 4 | 5 | **[Zero to JupyterHub with Kubernetes]** provides detailed instructions for using this project within a JupyerHub deployment. 6 | 7 | ## Overview of [Kubernetes] terminology 8 | 9 | ### What is [helm]? 10 | 11 | [helm] is the Kubernetes package manager. [Helm] streamlines installing and managing Kubernetes applications. _Reference: [helm repo]_ 12 | 13 | ### What is a [chart]? 14 | 15 | Charts are Helm packages that contain at least two things: 16 | 17 | - A description of the package (`Chart.yaml`) 18 | - One or more **templates**, which contain Kubernetes manifest files 19 | 20 | _Reference: [Kubernetes Introduction to charts]_ 21 | 22 | ## Contents of this repository 23 | 24 | ### `binderhub` folder 25 | 26 | Fundamental elements of a chart including: 27 | 28 | - `templates` folder 29 | - `Chart.yaml.template` 30 | - `values.yaml` 31 | 32 | ### `images` folder 33 | 34 | Docker images for applications including: 35 | 36 | - `binderhub` 37 | 38 | ### `chartpress` 39 | 40 | Useful for compiling custom charts. 41 | 42 | ## Usage 43 | 44 | In the helm-chart directory: 45 | 46 | chartpress 47 | 48 | to build the docker images and rerender the helm chart. 49 | 50 | [binderhub]: https://binderhub.readthedocs.io/en/latest/ 51 | [jupyterhub]: https://jupyterhub.readthedocs.io/en/latest/ 52 | [kubernetes]: https://kubernetes.io 53 | [helm]: https://helm.sh/ 54 | [helm repo]: https://github.com/kubernetes/helm 55 | [chart]: https://helm.sh/docs/topics/charts/ 56 | [kubernetes introduction to charts]: https://helm.sh/docs/topics/charts/ 57 | [zero to jupyterhub with kubernetes]: https://zero-to-jupyterhub.readthedocs.io/en/latest/ 58 | -------------------------------------------------------------------------------- /helm-chart/binderhub/.helmignore: -------------------------------------------------------------------------------- 1 | # Anything within the root folder of the Helm chart, where Chart.yaml resides, 2 | # will be embedded into the packaged Helm chart. This is reasonable since only 3 | # when the templates render after the chart has been packaged and distributed, 4 | # will the templates logic evaluate that determines if other files were 5 | # referenced, such as our our files/hub/jupyterhub_config.py. 6 | # 7 | # Here are files that we intentionally ignore to avoid them being packaged, 8 | # because we don't want to reference them from our templates anyhow. 9 | schema.yaml 10 | 11 | # Patterns to ignore when building packages. 12 | # This supports shell glob matching, relative path matching, and 13 | # negation (prefixed with !). Only one pattern per line. 14 | .DS_Store 15 | # Common VCS dirs 16 | .git/ 17 | .gitignore 18 | .bzr/ 19 | .bzrignore 20 | .hg/ 21 | .hgignore 22 | .svn/ 23 | # Common backup files 24 | *.swp 25 | *.bak 26 | *.tmp 27 | *~ 28 | # Various IDEs 29 | .project 30 | .idea/ 31 | *.tmproj 32 | -------------------------------------------------------------------------------- /helm-chart/binderhub/Chart.yaml: -------------------------------------------------------------------------------- 1 | # Chart.yaml v2 reference: https://helm.sh/docs/topics/charts/#the-chartyaml-file 2 | apiVersion: v2 3 | name: binderhub 4 | version: 0.0.1-set.by.chartpress 5 | dependencies: 6 | # Source code: https://github.com/jupyterhub/zero-to-jupyterhub-k8s 7 | # Latest version: https://github.com/jupyterhub/zero-to-jupyterhub-k8s/tags 8 | # App changelog: https://github.com/jupyterhub/zero-to-jupyterhub-k8s/tree/HEAD/CHANGELOG.md 9 | # 10 | # Important: Whenever you bump the jupyterhub Helm chart version, also inspect 11 | # the helm-chart/images/binderhub pinned version in requirements.in 12 | # and run "./dependencies freeze --upgrade". 13 | # 14 | - name: jupyterhub 15 | version: "4.0.0" 16 | repository: "https://jupyterhub.github.io/helm-chart" 17 | description: |- 18 | BinderHub is like a JupyterHub that automatically builds environments for the 19 | users based on repo2docker. A BinderHub is by default not configured to 20 | authenticate users or provide storage for them. 21 | keywords: [jupyter, jupyterhub, binderhub] 22 | home: https://binderhub.readthedocs.io/en/latest/ 23 | sources: [https://github.com/jupyterhub/binderhub] 24 | icon: https://jupyterhub.github.io/helm-chart/images/hublogo.svg 25 | kubeVersion: ">=1.28.0-0" 26 | maintainers: 27 | # Since it is a requirement of Artifact Hub to have specific maintainers 28 | # listed, we have added some below, but in practice the entire JupyterHub team 29 | # contributes to the maintenance of this Helm chart. Please go ahead and add 30 | # yourself! 31 | - name: Yuvi 32 | email: yuvipanda@gmail.com 33 | - name: Erik Sundell 34 | email: erik@sundellopensource.se 35 | -------------------------------------------------------------------------------- /helm-chart/binderhub/files/binderhub_config.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from urllib.parse import urlparse 3 | 4 | from ruamel.yaml import YAML 5 | 6 | yaml = YAML() 7 | 8 | 9 | # memoize so we only load config once 10 | @lru_cache 11 | def _load_values(): 12 | """Load configuration from disk 13 | 14 | Memoized to only load once 15 | """ 16 | path = "/etc/binderhub/config/values.yaml" 17 | print(f"Loading {path}") 18 | with open(path) as f: 19 | return yaml.load(f) 20 | 21 | 22 | def get_value(key, default=None): 23 | """ 24 | Find an item in values.yaml of a given name & return it 25 | 26 | get_value("a.b.c") returns values['a']['b']['c'] 27 | """ 28 | # start at the top 29 | value = _load_values() 30 | # resolve path in yaml 31 | for level in key.split("."): 32 | if not isinstance(value, dict): 33 | # a parent is a scalar or null, 34 | # can't resolve full path 35 | return default 36 | if level not in value: 37 | return default 38 | else: 39 | value = value[level] 40 | return value 41 | 42 | 43 | # load custom templates, by default 44 | c.BinderHub.template_path = "/etc/binderhub/templates" 45 | 46 | # load config from values.yaml 47 | for section, sub_cfg in get_value("config", {}).items(): 48 | c[section].update(sub_cfg) 49 | 50 | imageBuilderType = get_value("imageBuilderType") 51 | if imageBuilderType in ["dind", "pink"]: 52 | hostSocketDir = get_value(f"{imageBuilderType}.hostSocketDir") 53 | if hostSocketDir: 54 | socketname = "docker" if imageBuilderType == "dind" else "podman" 55 | c.BinderHub.build_docker_host = f"unix://{hostSocketDir}/{socketname}.sock" 56 | 57 | if c.BinderHub.auth_enabled: 58 | if "hub_url" not in c.BinderHub: 59 | c.BinderHub.hub_url = "" 60 | hub_url = urlparse(c.BinderHub.hub_url) 61 | c.HubOAuth.hub_host = f"{hub_url.scheme}://{hub_url.netloc}" 62 | if "base_url" in c.BinderHub: 63 | c.HubOAuth.base_url = c.BinderHub.base_url 64 | 65 | # load extra config snippets 66 | for key, snippet in sorted((get_value("extraConfig") or {}).items()): 67 | print(f"Loading extra config: {key}") 68 | exec(snippet) 69 | -------------------------------------------------------------------------------- /helm-chart/binderhub/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | */}} 13 | {{- define "fullname" -}} 14 | {{- $name := default .Chart.Name .Values.nameOverride -}} 15 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 16 | {{- end -}} 17 | 18 | {{/* 19 | Render docker config.json for the registry-publishing secret and other docker configuration. 20 | */}} 21 | {{- define "buildDockerConfig" -}} 22 | 23 | {{- /* default auth url */ -}} 24 | {{- $url := (default "https://index.docker.io/v1/" .Values.registry.url) }} 25 | 26 | {{- /* default username if unspecified 27 | (_json_key for gcr.io, otherwise) 28 | */ -}} 29 | 30 | {{- if not .Values.registry.username }} 31 | {{- if eq $url "https://gcr.io" }} 32 | {{- $_ := set .Values.registry "username" "_json_key" }} 33 | {{- else }} 34 | {{- $_ := set .Values.registry "username" "" }} 35 | {{- end }} 36 | {{- end }} 37 | {{- $username := .Values.registry.username -}} 38 | 39 | {{- /* initialize a dict to represent a docker config with registry credentials */}} 40 | {{- $dockerConfig := dict "auths" (dict $url (dict "auth" (printf "%s:%s" $username .Values.registry.password | b64enc))) }} 41 | 42 | {{- /* augment our initialized docker config with buildDockerConfig */}} 43 | {{- if .Values.config.BinderHub.buildDockerConfig }} 44 | {{- $dockerConfig := merge $dockerConfig .Values.config.BinderHub.buildDockerConfig }} 45 | {{- end }} 46 | 47 | {{- $dockerConfig | toPrettyJson }} 48 | {{- end }} 49 | -------------------------------------------------------------------------------- /helm-chart/binderhub/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | name: binderhub 6 | {{- if or (and .Values.ingress.https.enabled (eq .Values.ingress.https.type "kube-lego")) .Values.ingress.annotations }} 7 | annotations: 8 | {{- if and .Values.ingress.https.enabled (eq .Values.ingress.https.type "kube-lego") }} 9 | kubernetes.io/tls-acme: "true" 10 | {{- end }} 11 | {{- with .Values.ingress.annotations }} 12 | {{- . | toYaml | nindent 4 }} 13 | {{- end }} 14 | {{- end }} 15 | spec: 16 | {{- with .Values.ingress.ingressClassName }} 17 | ingressClassName: {{ . | quote }} 18 | {{- end }} 19 | rules: 20 | {{- range $host := .Values.ingress.hosts | default (list "") }} 21 | - http: 22 | paths: 23 | - path: /{{ $.Values.ingress.pathSuffix }} 24 | pathType: {{ $.Values.ingress.pathType }} 25 | backend: 26 | service: 27 | name: binder 28 | port: 29 | number: 80 30 | {{- with $host }} 31 | host: {{ . | quote }} 32 | {{- end }} 33 | {{- end }} 34 | {{- if and .Values.ingress.https.enabled (eq .Values.ingress.https.type "kube-lego") }} 35 | tls: 36 | - secretName: kubelego-tls-binder-{{ .Release.Name }} 37 | hosts: 38 | {{- range .Values.ingress.hosts }} 39 | - {{ . | quote }} 40 | {{- end }} 41 | {{- else if .Values.ingress.tls }} 42 | tls: 43 | {{- range .Values.ingress.tls }} 44 | - hosts: 45 | {{- range .hosts }} 46 | - {{ . | quote }} 47 | {{- end }} 48 | secretName: {{ .secretName }} 49 | {{- end }} 50 | {{- end }} 51 | {{- end }} 52 | -------------------------------------------------------------------------------- /helm-chart/binderhub/templates/pdb.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.pdb.enabled -}} 2 | {{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} 3 | apiVersion: policy/v1 4 | {{- else }} 5 | apiVersion: policy/v1beta1 6 | {{- end }} 7 | kind: PodDisruptionBudget 8 | metadata: 9 | name: binderhub 10 | labels: 11 | app: binder 12 | name: binder 13 | component: binder 14 | release: {{ .Release.Name }} 15 | heritage: {{ .Release.Service }} 16 | spec: 17 | {{- if not (.Values.pdb.maxUnavailable | typeIs "") }} 18 | maxUnavailable: {{ .Values.pdb.maxUnavailable }} 19 | {{- end }} 20 | {{- if not (.Values.pdb.minAvailable | typeIs "") }} 21 | minAvailable: {{ .Values.pdb.minAvailable }} 22 | {{- end }} 23 | selector: 24 | matchLabels: 25 | app: binder 26 | name: binder 27 | component: binder 28 | release: {{ .Release.Name }} 29 | {{- end }} 30 | -------------------------------------------------------------------------------- /helm-chart/binderhub/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.enabled -}} 2 | kind: Role 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | labels: 6 | chart: {{ .Chart.Name }}-{{ .Chart.Version }} 7 | heritage: {{ .Release.Service }} 8 | release: {{ .Release.Name }} 9 | name: binderhub 10 | rules: 11 | - apiGroups: [""] # "" indicates the core API group 12 | resources: ["pods"] 13 | verbs: ["get", "watch", "list", "create", "delete"] 14 | - apiGroups: [""] 15 | resources: ["pods/log"] 16 | verbs: ["get"] 17 | --- 18 | kind: RoleBinding 19 | apiVersion: rbac.authorization.k8s.io/v1 20 | metadata: 21 | labels: 22 | app: binderhub 23 | chart: {{ .Chart.Name }}-{{ .Chart.Version }} 24 | heritage: {{ .Release.Service }} 25 | release: {{ .Release.Name }} 26 | name: binderhub 27 | subjects: 28 | - kind: ServiceAccount 29 | namespace: {{ .Release.Namespace }} 30 | name: binderhub 31 | roleRef: 32 | kind: Role 33 | name: binderhub 34 | apiGroup: rbac.authorization.k8s.io 35 | --- 36 | apiVersion: v1 37 | kind: ServiceAccount 38 | metadata: 39 | labels: 40 | app: binderhub 41 | chart: {{ .Chart.Name }}-{{ .Chart.Version }} 42 | heritage: {{ .Release.Service }} 43 | release: {{ .Release.Name }} 44 | name: binderhub 45 | {{- if .Values.imageCleaner.enabled }} 46 | --- 47 | # image-cleaner role 48 | # needs to cordon nodes during image cleaning 49 | kind: ClusterRole 50 | apiVersion: rbac.authorization.k8s.io/v1 51 | metadata: 52 | labels: 53 | app: binderhub 54 | chart: {{ .Chart.Name }}-{{ .Chart.Version }} 55 | heritage: {{ .Release.Service }} 56 | release: {{ .Release.Name }} 57 | name: {{ .Release.Name }}-image-cleaner 58 | rules: 59 | - apiGroups: [""] # "" indicates the core API group 60 | resources: ["nodes"] 61 | verbs: ["get", "patch"] 62 | --- 63 | kind: ClusterRoleBinding 64 | apiVersion: rbac.authorization.k8s.io/v1 65 | metadata: 66 | labels: 67 | app: binderhub 68 | chart: {{ .Chart.Name }}-{{ .Chart.Version }} 69 | heritage: {{ .Release.Service }} 70 | release: {{ .Release.Name }} 71 | name: {{ .Release.Name }}-image-cleaner 72 | subjects: 73 | - kind: ServiceAccount 74 | namespace: {{ .Release.Namespace }} 75 | name: {{ .Release.Name }}-image-cleaner 76 | roleRef: 77 | kind: ClusterRole 78 | name: {{ .Release.Name }}-image-cleaner 79 | apiGroup: rbac.authorization.k8s.io 80 | --- 81 | apiVersion: v1 82 | kind: ServiceAccount 83 | metadata: 84 | labels: 85 | app: binderhub 86 | chart: {{ .Chart.Name }}-{{ .Chart.Version }} 87 | heritage: {{ .Release.Service }} 88 | release: {{ .Release.Name }} 89 | name: {{ .Release.Name }}-image-cleaner 90 | {{- end }} 91 | {{- end }} 92 | -------------------------------------------------------------------------------- /helm-chart/binderhub/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- /* 2 | Note that changes to the rendered version of this 3 | file will trigger a restart of the BinderHub pod 4 | through a annotation containing a hash of this 5 | file rendered. 6 | */ -}} 7 | kind: Secret 8 | apiVersion: v1 9 | metadata: 10 | name: binder-secret 11 | type: Opaque 12 | stringData: 13 | {{- /* 14 | Stash away relevant Helm template values for 15 | the BinderHub Python application to read from 16 | in binderhub_config.py. 17 | */}} 18 | values.yaml: | 19 | {{- pick .Values "config" "imageBuilderType" "cors" "dind" "pink" "extraConfig" | toYaml | nindent 4 }} 20 | 21 | {{- /* Glob files to allow them to be mounted by the binderhub pod */}} 22 | {{- /* key=filename: value=content */}} 23 | {{- (.Files.Glob "files/*").AsConfig | nindent 2 }} 24 | 25 | {{- with include "jupyterhub.extraFiles.stringData" .Values.extraFiles }} 26 | {{- . | nindent 2 }} 27 | {{- end }} 28 | 29 | {{- with include "jupyterhub.extraFiles.data" .Values.extraFiles }} 30 | data: 31 | {{- . | nindent 2 }} 32 | {{- end }} 33 | --- 34 | {{- if or .Values.config.BinderHub.use_registry .Values.config.BinderHub.buildDockerConfig }} 35 | kind: Secret 36 | apiVersion: v1 37 | metadata: 38 | name: binder-build-docker-config 39 | type: Opaque 40 | data: 41 | config.json: {{ include "buildDockerConfig" . | b64enc | quote }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /helm-chart/binderhub/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: binder 5 | annotations: {{ .Values.service.annotations | toJson }} 6 | labels: {{ .Values.service.labels | toJson }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | {{- with .Values.service.loadBalancerIP }} 10 | loadBalancerIP: {{ . | quote }} 11 | {{- end }} 12 | selector: 13 | app: binder 14 | name: binder 15 | component: binder 16 | release: {{ .Release.Name }} 17 | heritage: {{ .Release.Service }} 18 | ports: 19 | - protocol: TCP 20 | port: 80 21 | targetPort: 8585 22 | {{- with .Values.service.nodePort }} 23 | nodePort: {{ . }} 24 | {{- end }} 25 | -------------------------------------------------------------------------------- /helm-chart/chartpress.yaml: -------------------------------------------------------------------------------- 1 | # For a reference on this configuration, see the chartpress README file. 2 | # ref: https://github.com/jupyterhub/chartpress 3 | # 4 | # NOTE: All paths will be set relative to this file's location, which is in the 5 | # helm-chart folder. 6 | charts: 7 | - name: binderhub 8 | baseVersion: 1.0.0-0.dev 9 | imagePrefix: quay.io/jupyterhub/k8s- 10 | repo: 11 | git: jupyterhub/helm-chart 12 | published: https://jupyterhub.github.io/helm-chart 13 | images: 14 | binderhub: 15 | # We will not use the default build contextPath, and must therefore 16 | # specify the dockerfilePath explicitly. 17 | dockerfilePath: images/binderhub/Dockerfile 18 | # Context to send to docker build for use by the Dockerfile. We pass the 19 | # root folder in order to allow the image to access and build the python 20 | # package. 21 | contextPath: .. 22 | # To avoid chartpress to react to changes in documentation and other 23 | # things, we ask it to not trigger on changes to the contextPath, which 24 | # means we manually should add paths rebuild should be triggered on 25 | rebuildOnContextPathChanges: false 26 | # We manually specify the paths which chartpress should monitor for 27 | # changes that should trigger a rebuild of this image. 28 | paths: 29 | - images/binderhub 30 | - ../binderhub 31 | - ../js 32 | - ../babel.config.json 33 | - ../MANIFEST.in 34 | - ../package.json 35 | - ../pyproject.toml 36 | - ../requirements.txt 37 | - ../setup.cfg 38 | - ../setup.py 39 | - ../webpack.config.js 40 | valuesPath: image 41 | -------------------------------------------------------------------------------- /helm-chart/images/binderhub/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.3 2 | 3 | 4 | # The build stage 5 | # --------------- 6 | # This stage is building Python wheels for use in later stages by using a base 7 | # image that has more pre-requisites to do so, such as a C++ compiler. 8 | # 9 | # NOTE: If the image version is updated, also update it in ci/refreeze! 10 | # 11 | FROM python:3.13-bookworm as build-stage 12 | 13 | # Build wheels 14 | # 15 | # We set pip's cache directory and expose it across build stages via an 16 | # ephemeral docker cache (--mount=type=cache,target=${PIP_CACHE_DIR}). We use 17 | # the same technique for the directory /tmp/wheels. 18 | # 19 | # assumes `python3 -m build .` has been run to create the wheel 20 | # in the top-level dist directory 21 | COPY helm-chart/images/binderhub/requirements.txt ./ 22 | COPY dist . 23 | ARG PIP_CACHE_DIR=/tmp/pip-cache 24 | RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ 25 | pip wheel --wheel-dir=/tmp/wheels \ 26 | # pycurl wheels for 7.45.3 have problems finding CAs 27 | # https://github.com/pycurl/pycurl/issues/834 28 | --no-binary pycurl \ 29 | -r ./requirements.txt \ 30 | ./binderhub*.whl 31 | 32 | 33 | # The final stage 34 | # --------------- 35 | # This stage is built and published as quay.io/jupyterhub/k8s-binderhub. 36 | # 37 | FROM python:3.13-slim-bookworm 38 | 39 | ENV PYTHONUNBUFFERED=1 40 | ENV DEBIAN_FRONTEND=noninteractive 41 | 42 | RUN apt-get update \ 43 | && apt-get upgrade --yes \ 44 | && apt-get install --yes \ 45 | git \ 46 | # required by binderhub 47 | libcurl4 \ 48 | # required by pycurl 49 | tini \ 50 | # tini is used as an entrypoint to not loose track of SIGTERM 51 | # signals as sent before SIGKILL, for example when "docker stop" 52 | # or "kubectl delete pod" is run. By doing that the pod can 53 | # terminate very quickly. 54 | && rm -rf /var/lib/apt/lists/* 55 | 56 | # install wheels built in the build stage 57 | COPY helm-chart/images/binderhub/requirements.txt /tmp/requirements.txt 58 | ARG PIP_CACHE_DIR=/tmp/pip-cache 59 | RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ 60 | --mount=type=cache,from=build-stage,source=/tmp/wheels,target=/tmp/wheels \ 61 | pip install --no-deps /tmp/wheels/* \ 62 | # validate pip install since it's not resolving dependencies 63 | && pip check \ 64 | # verify success of previous step 65 | && python -c "import pycurl, binderhub.app" 66 | 67 | EXPOSE 8585 68 | ENTRYPOINT ["tini", "--", "python", "-m", "binderhub"] 69 | CMD ["--config", "/etc/binderhub/config/binderhub_config.py"] 70 | -------------------------------------------------------------------------------- /helm-chart/images/binderhub/README.md: -------------------------------------------------------------------------------- 1 | # binderhub image 2 | 3 | The image for running binderhub itself. 4 | Built with [chartpress][]. 5 | 6 | [chartpress]: https://github.com/jupyterhub/chartpress 7 | 8 | ## Updating requirements.txt 9 | 10 | Use the "Run workflow" button at 11 | https://github.com/jupyterhub/binderhub/actions/workflows/watch-dependencies.yaml. 12 | -------------------------------------------------------------------------------- /helm-chart/images/binderhub/requirements.in: -------------------------------------------------------------------------------- 1 | # google-cloud-logging is an optional dependency used by mybinder.org-deploy at 2 | # https://github.com/jupyterhub/mybinder.org-deploy/blob/e47021fe/mybinder/values.yaml#L193-L216. 3 | # 4 | # We pin it to avoid introducing a potentially breaking change as major versions 5 | # are released. See: 6 | # https://github.com/googleapis/python-logging/blob/master/UPGRADING.md 7 | # 8 | google-cloud-logging==3.* 9 | 10 | # jupyterhub's major version should be matched with the JupyterHub Helm chart's 11 | # used version of JupyterHub. 12 | # 13 | jupyterhub==4.* 14 | 15 | # https://github.com/kubernetes-client/python 16 | kubernetes==9.* 17 | 18 | # binderhub's dependencies 19 | # 20 | # We can't put ".[pycurl]" here directly as when we freeze this into 21 | # requirements.txt using ci/refreeze, its declaring "binderhub @ file:///io" 22 | # which is a problem as its an absolute path. 23 | # 24 | pycurl 25 | -r ../../../requirements.txt 26 | -------------------------------------------------------------------------------- /integration-tests/conftest.py: -------------------------------------------------------------------------------- 1 | import nest_asyncio 2 | 3 | 4 | def pytest_configure(config): 5 | # Required for playwright to be run from within pytest 6 | nest_asyncio.apply() 7 | -------------------------------------------------------------------------------- /js/packages/binderhub-client/README.md: -------------------------------------------------------------------------------- 1 | # `binderhub-client` 2 | 3 | A simple way to access the [BinderHub API](https://binderhub.readthedocs.io/en/latest/api.html) 4 | -------------------------------------------------------------------------------- /js/packages/binderhub-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyterhub/binderhub-client", 3 | "version": "0.4.0", 4 | "description": "Simple API client for the BinderHub EventSource API", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/jupyterhub/binderhub.git" 9 | }, 10 | "author": "Project Jupyter Contributors", 11 | "license": "BSD-3-Clause", 12 | "bugs": { 13 | "url": "https://github.com/jupyterhub/binderhub/issues" 14 | }, 15 | "homepage": "https://github.com/jupyterhub/binderhub#readme", 16 | "dependencies": { 17 | "@microsoft/fetch-event-source": "^2.0.1", 18 | "event-iterator": "^2.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /js/packages/binderhub-client/tests/utils.js: -------------------------------------------------------------------------------- 1 | import { createServer } from "node:http"; 2 | 3 | /** 4 | * Parse an existing stored EventSource response body into an array of JSON objects 5 | * 6 | * @param {string} responseBody Full text of the response to parse as EventSource formatted data 7 | */ 8 | export function parseEventSource(responseBody) { 9 | let messages = []; 10 | for (const line of responseBody.split("\n")) { 11 | const part = line.slice("data: ".length - 1); 12 | if (part.trim() !== "") { 13 | messages.push(JSON.parse(part)); 14 | } 15 | } 16 | return messages; 17 | } 18 | 19 | /** 20 | * Temporarily start a HTTP server to serve EventSource resources 21 | * 22 | * Returns the serverURL (including the protocol) where the server is listening, as well 23 | * as a function that can be used to stop the server. 24 | * 25 | * @param {object} fakeResponses Mapping of paths to response bodies (in EventSource format) 26 | * the server should respond with when those paths are requested. All 27 | * other paths will get a 404 response. 28 | * @returns {Promise} 29 | */ 30 | export async function simpleEventSourceServer(fakeResponses) { 31 | return new Promise((resolve) => { 32 | const server = createServer(async (req, res) => { 33 | if (fakeResponses[req.url]) { 34 | res.statusCode = 200; 35 | res.setHeader("Content-Type", "text/event-stream"); 36 | // Setup CORS so jest can actually read the data 37 | res.setHeader("Access-Control-Allow-Origin", "*"); 38 | res.flushHeaders(); 39 | for (const line of fakeResponses[req.url].split("\n")) { 40 | // Eventsource format requires newlines between each line of message 41 | res.write(line + "\n\n"); 42 | // Wait at least 1ms between lines, to simulate all the data not arriving at once 43 | await new Promise((resolve) => setTimeout(resolve, 1)); 44 | } 45 | res.end(); 46 | } else { 47 | res.statusCode = 404; 48 | res.end(); 49 | } 50 | }); 51 | 52 | server.listen(0, "127.0.0.1", () => { 53 | resolve([ 54 | `http://${server.address().address}:${server.address().port}`, 55 | () => server.close(), 56 | ]); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "binderhub", 3 | "description": "Frontend Interface for BinderHub", 4 | "private": true, 5 | "dependencies": { 6 | "@fontsource/clear-sans": "^5.0.11", 7 | "bootstrap": "^5.3.3", 8 | "bootstrap-icons": "^1.11.3", 9 | "copy-to-clipboard": "^3.3.3", 10 | "react": "^19.0.0", 11 | "react-dom": "^19.0.0", 12 | "wouter": "^3.3.5", 13 | "@xterm/xterm": "^5.5.0", 14 | "@xterm/addon-fit": "^0.9.0" 15 | }, 16 | "devDependencies": { 17 | "@babel/cli": "^7.21.0", 18 | "@babel/core": "^7.21.4", 19 | "@babel/eslint-parser": "^7.22.15", 20 | "@babel/preset-env": "^7.21.4", 21 | "@babel/preset-react": "^7.26.3", 22 | "@types/react": "^19.0.0", 23 | "@testing-library/jest-dom": "^6.6.3", 24 | "@testing-library/react": "^16.1.0", 25 | "@testing-library/user-event": "^14.5.2", 26 | "configurable-http-proxy": "^4.6.2", 27 | "@types/jest": "^29.5.5", 28 | "@whatwg-node/fetch": "^0.9.17", 29 | "autoprefixer": "^10.4.19", 30 | "babel-jest": "^29.7.0", 31 | "babel-loader": "^9.1.2", 32 | "css-loader": "^6.11.0", 33 | "eslint": "^8.38.0", 34 | "eslint-plugin-jest": "^27.4.2", 35 | "eslint-plugin-react": "^7.37.2", 36 | "identity-obj-proxy": "^3.0.0", 37 | "jest": "^29.7.0", 38 | "jest-environment-jsdom": "^29.7.0", 39 | "mini-css-extract-plugin": "^2.7.5", 40 | "postcss-loader": "^8.1.1", 41 | "sass": "^1.77.1", 42 | "sass-loader": "^14.2.1", 43 | "style-loader": "^4.0.0", 44 | "ts-loader": "^9.5.1", 45 | "typescript": "^5.4.5", 46 | "webpack": "^5.78.0", 47 | "webpack-cli": "^5.0.1" 48 | }, 49 | "workspaces": [ 50 | "js/packages/binderhub-client" 51 | ], 52 | "scripts": { 53 | "webpack": "webpack", 54 | "webpack:watch": "webpack --watch", 55 | "lint": "eslint binderhub/static/js js", 56 | "test": "jest" 57 | }, 58 | "jest": { 59 | "testEnvironment": "jsdom", 60 | "collectCoverage": true, 61 | "coverageReporters": [ 62 | "text", 63 | "cobertura" 64 | ], 65 | "testPathIgnorePatterns": [ 66 | "spec.js" 67 | ], 68 | "moduleNameMapper": { 69 | "\\.css$": "identity-obj-proxy", 70 | "\\.scss$": "identity-obj-proxy", 71 | "\\.ico$": "identity-obj-proxy" 72 | }, 73 | "setupFilesAfterEnv": [ 74 | "/setupTests.js" 75 | ], 76 | "transformIgnorePatterns": [ 77 | "/node_modules/(?!wouter)" 78 | ], 79 | "transform": { 80 | "\\.[jt]sx?$": "babel-jest" 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "jupyter-packaging >= 0.10", 4 | "setuptools >= 40.9.0", 5 | "wheel", 6 | ] 7 | build-backend = "setuptools.build_meta" 8 | 9 | 10 | # black is used for autoformatting Python code 11 | [tool.black] 12 | # target-version should be all supported versions, see 13 | # https://github.com/psf/black/issues/751#issuecomment-473066811 14 | target-version = ["py310", "py311", "py312"] 15 | 16 | 17 | # The default isort output conflicts with black autoformatting. 18 | # Tell isort to behave nicely with black 19 | # See https://pycqa.github.io/isort/docs/configuration/black_compatibility.html 20 | # for more information. 21 | [tool.isort] 22 | profile = "black" 23 | 24 | 25 | # pytest is used for running Python based tests 26 | [tool.pytest.ini_options] 27 | # Run playwright tests only on firefox 28 | # Retain playwright traces after failing tests, to help with debugging 29 | addopts = "--verbose --color=yes --durations=10 --browser firefox --tracing retain-on-failure" 30 | asyncio_mode = "auto" 31 | testpaths = ["tests"] 32 | timeout = "60" 33 | 34 | # pytest-cov / coverage is used to measure code coverage of tests 35 | [tool.coverage.run] 36 | omit = [ 37 | "binderhub/tests/*", 38 | "binderhub/_version.py", 39 | "versioneer.py", 40 | ] 41 | parallel = true 42 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | docker 2 | escapism 3 | jinja2 4 | jsonschema 5 | jupyterhub 6 | kubernetes 7 | prometheus_client 8 | pyjwt>=2 9 | python-json-logger 10 | tornado>=5.1 11 | traitlets 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [versioneer] 2 | VCS = git 3 | style = pep440 4 | versionfile_source = binderhub/_version.py 5 | versionfile_build = binderhub/_version.py 6 | tag_prefix = 7 | parentdir_prefix = binderhub- 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from jupyter_packaging import npm_builder, wrap_installers 5 | from setuptools import find_packages, setup 6 | 7 | # ensure the current directory is on sys.path 8 | # so versioneer can be imported when pip uses 9 | # PEP 517/518 build rules. 10 | # https://github.com/python-versioneer/python-versioneer/issues/193 11 | sys.path.append(os.path.dirname(__file__)) 12 | 13 | import versioneer 14 | 15 | here = os.path.dirname(__file__) 16 | 17 | # Representative files that should exist after a successful build 18 | jstargets = [ 19 | os.path.join(here, "binderhub", "static", "dist", "bundle.js"), 20 | ] 21 | 22 | # Automatically rebuild assets in dist if js is modified 23 | jsdeps = npm_builder( 24 | build_cmd="webpack", 25 | build_dir="binderhub/static/dist/", 26 | source_dir="binderhub/static/js/", 27 | ) 28 | cmdclass = wrap_installers( 29 | pre_develop=jsdeps, pre_dist=jsdeps, ensured_targets=jstargets 30 | ) 31 | 32 | 33 | with open(os.path.join(here, "requirements.txt")) as f: 34 | requirements = [ 35 | line.strip() for line in f.readlines() if not line.strip().startswith("#") 36 | ] 37 | 38 | with open(os.path.join(here, "README.md"), encoding="utf8") as f: 39 | readme = f.read() 40 | 41 | setup( 42 | name="binderhub", 43 | version=versioneer.get_version(), 44 | cmdclass=versioneer.get_cmdclass(cmdclass), 45 | python_requires=">=3.10", 46 | author="Project Jupyter Contributors", 47 | author_email="jupyter@googlegroups.com", 48 | license="BSD", 49 | url="https://binderhub.readthedocs.io/en/latest/", 50 | project_urls={ 51 | "Documentation": "https://binderhub.readthedocs.io/en/latest/", 52 | "Funding": "https://jupyter.org/about", 53 | "Source": "https://github.com/jupyterhub/binderhub/", 54 | "Tracker": "https://github.com/jupyterhub/binderhub/issues", 55 | }, 56 | # this should be a whitespace separated string of keywords, not a list 57 | keywords="reproducible science environments docker kubernetes", 58 | description="Turn a Git repo into a collection of interactive notebooks", 59 | long_description=readme, 60 | long_description_content_type="text/markdown", 61 | packages=find_packages(), 62 | include_package_data=True, 63 | install_requires=requirements, 64 | extras_require={ 65 | # pycurl is an optional dependency which improves performance 66 | # 67 | # - pycurl requires both `curl-config` and `gcc` to be available when 68 | # installing it from source. 69 | # - pycurl should always be used in production, but it's not relevant 70 | # for building documentation which inspects the source code. 71 | # 72 | "pycurl": ["pycurl"], 73 | }, 74 | ) 75 | -------------------------------------------------------------------------------- /setupTests.js: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals"; 2 | import "@testing-library/jest-dom"; 3 | 4 | HTMLCanvasElement.prototype.getContext = () => {}; 5 | Object.defineProperty(window, "matchMedia", { 6 | writable: true, 7 | value: jest.fn().mockImplementation((query) => ({ 8 | matches: false, 9 | media: query, 10 | onchange: null, 11 | addListener: jest.fn(), // Deprecated 12 | removeListener: jest.fn(), // Deprecated 13 | addEventListener: jest.fn(), 14 | removeEventListener: jest.fn(), 15 | dispatchEvent: jest.fn(), 16 | })), 17 | }); 18 | 19 | window.pageConfig = { 20 | baseUrl: "/", 21 | aboutMessage: "This is the about message", 22 | binderVersion: "v123.456", 23 | repoProviders: [ 24 | { 25 | detect: { 26 | regex: "^(https?://github.com/)?(?.*)", 27 | }, 28 | displayName: "GitHub", 29 | id: "gh", 30 | spec: { validateRegex: ".+\\/.+\\/.+" }, 31 | ref: { 32 | default: "HEAD", 33 | enabled: true, 34 | }, 35 | repo: { 36 | label: "GitHub repository name or URL", 37 | placeholder: 38 | "example: yuvipanda/requirements or https://github.com/yuvipanda/requirements", 39 | }, 40 | }, 41 | { 42 | displayName: "Zenodo DOI", 43 | id: "zenodo", 44 | spec: { validateRegex: "10\\.\\d+\\/(.)+" }, 45 | ref: { 46 | enabled: false, 47 | }, 48 | repo: { 49 | label: "Zenodo DOI", 50 | placeholder: "example: 10.5281/zenodo.3242074", 51 | }, 52 | }, 53 | ], 54 | }; 55 | -------------------------------------------------------------------------------- /testing/k8s-binder-k8s-hub/binderhub-chart+dind.yaml: -------------------------------------------------------------------------------- 1 | # Additional configuration for testing dind 2 | # You must create configmap/insecure-registries-dind first to allow testing with an 3 | # insecure http registry 4 | # https://docs.docker.com/registry/insecure/ 5 | 6 | config: 7 | BinderHub: 8 | use_registry: true 9 | 10 | imageBuilderType: dind 11 | 12 | dind: 13 | daemonset: 14 | extraVolumeMounts: 15 | - name: insecure-registries-dind 16 | mountPath: /etc/docker/daemon.json 17 | subPath: daemon.json 18 | extraVolumes: 19 | - name: insecure-registries-dind 20 | configMap: 21 | name: insecure-registries-dind 22 | -------------------------------------------------------------------------------- /testing/k8s-binder-k8s-hub/binderhub-chart+pink.yaml: -------------------------------------------------------------------------------- 1 | # Additional configuration for testing podman 2 | # You must create configmap/insecure-registries-pink first to allow testing with an 3 | # insecure http registry 4 | 5 | config: 6 | BinderHub: 7 | use_registry: true 8 | 9 | imageBuilderType: pink 10 | 11 | pink: 12 | daemonset: 13 | extraArgs: 14 | - --log-level=debug 15 | extraVolumeMounts: 16 | - name: insecure-registries-pink 17 | mountPath: /etc/containers/registries.conf.d/100-insecure-registries-pink.conf 18 | subPath: 100-insecure-registries-pink.conf 19 | extraVolumes: 20 | - name: insecure-registries-pink 21 | configMap: 22 | name: insecure-registries-pink 23 | -------------------------------------------------------------------------------- /testing/k8s-binder-k8s-hub/binderhub-chart-config-old.yaml: -------------------------------------------------------------------------------- 1 | # (DELETE ME) 2 | # this file should be removed immediately after merging PR #1472 3 | 4 | service: 5 | type: NodePort 6 | nodePort: 30901 7 | 8 | config: 9 | BinderHub: 10 | # Use the internal host name for Pod to Pod communication 11 | # We can't use `hub_url` here because that is set to localhost which 12 | # works on the host but not from within a Pod 13 | hub_url_local: http://proxy-public 14 | use_registry: false 15 | log_level: 10 16 | cors_allow_origin: "*" 17 | 18 | ingress: 19 | # Enabled to test the creation/update of the k8s Ingress resource, but not 20 | # used actively in our CI system. 21 | enabled: true 22 | 23 | # Not using dind to test the creation/update of the image-cleaner DaemonSet 24 | # resource because it also requires us to setup a container registry to test 25 | # against which we haven't. We currently only test this via 26 | # lint-and-validate-values.yaml that makes sure our rendered templates are 27 | # valid against a k8s api-server. 28 | imageBuilderType: host 29 | 30 | # NOTE: This is a mirror of the jupyterhub section in 31 | # jupyterhub-chart-config.yaml in testing/local-binder-k8s-hub, keep these 32 | # two files synced please. 33 | jupyterhub: 34 | debug: 35 | enabled: true 36 | 37 | hub: 38 | config: 39 | BinderSpawner: 40 | cors_allow_origin: "*" 41 | db: 42 | type: "sqlite-memory" 43 | 44 | proxy: 45 | service: 46 | type: NodePort 47 | nodePorts: 48 | http: 30902 49 | 50 | singleuser: 51 | storage: 52 | type: none 53 | memory: 54 | guarantee: null 55 | -------------------------------------------------------------------------------- /testing/k8s-binder-k8s-hub/binderhub-chart-config.yaml: -------------------------------------------------------------------------------- 1 | # This config is used when both BinderHub and the JupyterHub it uses are 2 | # deployed to a kubernetes cluster. 3 | # note: when changing the config schema, 4 | # the old version of this file may need to be copied to ./binderhub-chart-config-old.yaml 5 | # before updating, and then deleted in a subsequent PR. 6 | 7 | service: 8 | type: NodePort 9 | nodePort: 30901 10 | 11 | config: 12 | BinderHub: 13 | # Use the internal host name for Pod to Pod communication 14 | # We can't use `hub_url` here because that is set to localhost which 15 | # works on the host but not from within a Pod 16 | hub_url_local: http://proxy-public 17 | use_registry: false 18 | log_level: 10 19 | cors_allow_origin: "*" 20 | 21 | ingress: 22 | # Enabled to test the creation/update of the k8s Ingress resource, but not 23 | # used actively in our CI system. 24 | enabled: true 25 | 26 | # No "in cluster" builder to test the creation/update of the image-cleaner DaemonSet 27 | # resource because it also requires us to setup a container registry to test 28 | # against which we haven't. We currently only test this through the use of 29 | # lint-and-validate-values.yaml and setting this value explicitly to make sure 30 | # our rendered templates are valid against a k8s api-server. 31 | # This is already the default 32 | # imageBuilderType: "host" 33 | 34 | # NOTE: This is a mirror of the jupyterhub section in 35 | # jupyterhub-chart-config.yaml in testing/local-binder-k8s-hub, keep these 36 | # two files synced please. 37 | jupyterhub: 38 | debug: 39 | enabled: true 40 | 41 | hub: 42 | config: 43 | BinderSpawner: 44 | cors_allow_origin: "*" 45 | db: 46 | type: "sqlite-memory" 47 | 48 | proxy: 49 | service: 50 | type: NodePort 51 | nodePorts: 52 | http: 30902 53 | 54 | singleuser: 55 | storage: 56 | type: none 57 | memory: 58 | guarantee: null 59 | -------------------------------------------------------------------------------- /testing/k8s-binder-k8s-hub/cm-insecure-registries-dind.yaml: -------------------------------------------------------------------------------- 1 | # REGISTRY_HOST='HOST:PORT' envsubst < cm-insecure-registries-dind.yaml | kubectl apply -f - 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: insecure-registries-dind 6 | data: 7 | daemon.json: | 8 | { 9 | "insecure-registries": ["$REGISTRY_HOST"], 10 | "debug": true 11 | } 12 | -------------------------------------------------------------------------------- /testing/k8s-binder-k8s-hub/cm-insecure-registries-pink.yaml: -------------------------------------------------------------------------------- 1 | # REGISTRY_HOST='HOST:PORT' envsubst < cm-insecure-registries-pink.yaml | kubectl apply -f - 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: insecure-registries-pink 6 | data: 7 | 100-insecure-registries-pink.conf: | 8 | # https://www.redhat.com/sysadmin/manage-container-registries 9 | unqualified-search-registries = ["docker.io"] 10 | [[registry]] 11 | location="$REGISTRY_HOST" 12 | insecure=true 13 | -------------------------------------------------------------------------------- /testing/local-binder-k8s-hub/binderhub_config.py: -------------------------------------------------------------------------------- 1 | # A development config to test a BinderHub deployment generally. It can be 2 | # combined with with the auth specific config. 3 | 4 | # Deployment assumptions: 5 | # - BinderHub: standalone local installation 6 | # - JupyterHub: standalone k8s installation 7 | 8 | import os 9 | import subprocess 10 | 11 | # We need to find out the IP at which BinderHub can reach the JupyterHub API 12 | # For local development we recommend the use of minikube, but on GitHub 13 | # Actions we use k3s. This means there are different ways of obtaining the IP. 14 | # GITHUB_RUN_ID is an environment variable only set inside GH Actions, we 15 | # don't care about its value, just that it is set 16 | in_github_actions = os.getenv("GITHUB_RUN_ID") is not None 17 | 18 | if in_github_actions: 19 | jupyterhub_ip = "localhost" 20 | 21 | else: 22 | try: 23 | jupyterhub_ip = subprocess.check_output(["minikube", "ip"], text=True).strip() 24 | except (subprocess.SubprocessError, FileNotFoundError): 25 | jupyterhub_ip = "192.168.1.100" 26 | 27 | c.BinderHub.debug = True 28 | c.BinderHub.hub_url = f"http://{jupyterhub_ip}:30902" 29 | c.BinderHub.hub_api_token = "dummy-binder-secret-token" 30 | c.BinderHub.use_registry = False 31 | -------------------------------------------------------------------------------- /testing/local-binder-k8s-hub/binderhub_config_auth_additions.py: -------------------------------------------------------------------------------- 1 | # A development config to test a BinderHub deployment that is relying on 2 | # JupyterHub's as an OAuth2 based Identity Provider (IdP) for Authentication and 3 | # Authorization. JupyterHub is configured with its own Authenticator. 4 | 5 | # Deployment assumptions: 6 | # - BinderHub: standalone local installation 7 | # - JupyterHub: standalone k8s installation 8 | 9 | import os 10 | from urllib.parse import urlparse 11 | 12 | # As this config file reference traitlet values (c.BinderHub.hub_api_token, 13 | # c.BinderHub.hub_url) set in the more general config file, it must load the 14 | # more general config file first. 15 | here = os.path.abspath(os.path.dirname(__file__)) 16 | load_subconfig(os.path.join(here, "binderhub_config.py")) 17 | 18 | # Additional auth related configuration 19 | c.BinderHub.base_url = "/" 20 | c.BinderHub.auth_enabled = True 21 | url = urlparse(c.BinderHub.hub_url) 22 | c.HubOAuth.hub_host = f"{url.scheme}://{url.netloc}" 23 | c.HubOAuth.api_token = c.BinderHub.hub_api_token 24 | c.HubOAuth.api_url = c.BinderHub.hub_url + "/hub/api/" 25 | c.HubOAuth.base_url = c.BinderHub.base_url 26 | c.HubOAuth.hub_prefix = c.BinderHub.base_url + "hub/" 27 | c.HubOAuth.oauth_redirect_uri = "http://127.0.0.1:8585/oauth_callback" 28 | c.HubOAuth.oauth_client_id = "service-binder" 29 | c.HubOAuth.access_scopes = {"access:services!service=binder"} 30 | -------------------------------------------------------------------------------- /testing/local-binder-k8s-hub/install-jupyterhub-chart: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Makes a standalone installation of the JupyterHub Helm chart of the version 4 | specified in the BinderHub Helm chart's Chart.yaml file, and use the 5 | configuration for the JupyterHub Helm chart nested in the BinderHub helm chart's 6 | configuration. 7 | """ 8 | import os 9 | import sys 10 | from subprocess import check_call 11 | from tempfile import NamedTemporaryFile 12 | 13 | from ruamel.yaml import YAML 14 | 15 | yaml = YAML() 16 | 17 | here = os.path.abspath(os.path.dirname(__file__)) 18 | helm_chart = os.path.join(here, os.pardir, os.pardir, "helm-chart") 19 | 20 | 21 | def _get_jupyterhub_dependency_version(): 22 | """ 23 | Extract JupyterHub Helm chart version from the BinderHub chart's 24 | Chart.yaml file that lists its chart dependencies. 25 | """ 26 | chart_yaml = os.path.join(helm_chart, "binderhub", "Chart.yaml") 27 | 28 | with open(chart_yaml) as f: 29 | dependecies = yaml.load(f) 30 | for dep in dependecies["dependencies"]: 31 | if dep["name"] == "jupyterhub": 32 | return dep["version"] 33 | else: 34 | raise ValueError( 35 | f"JupyterHub as a Helm chart dependency not found in {chart_yaml}:\n{dependecies}" 36 | ) 37 | 38 | 39 | with NamedTemporaryFile(mode="w") as tmp: 40 | with open(os.path.join(helm_chart, "binderhub", "values.yaml")) as values_in: 41 | jupyterhub_chart_config = yaml.load(values_in)["jupyterhub"] 42 | yaml.dump(jupyterhub_chart_config, tmp.file) 43 | tmp.flush() 44 | 45 | cmd = ["helm", "upgrade", "--install", "binderhub-test"] 46 | cmd.extend( 47 | [ 48 | "jupyterhub", 49 | "--repo=https://jupyterhub.github.io/helm-chart/", 50 | f"--version={_get_jupyterhub_dependency_version()}", 51 | f"--values={tmp.name}", 52 | f'--values={os.path.join(here, "jupyterhub-chart-config.yaml")}', 53 | ] 54 | ) 55 | if "--auth" in sys.argv: 56 | cmd.extend( 57 | [ 58 | f'--values={os.path.join(here, "jupyterhub-chart-config-auth-additions.yaml")}' 59 | ] 60 | ) 61 | print("Installing the JupyterHub Helm chart by itself") 62 | print(" ".join(cmd)) 63 | check_call(cmd) 64 | -------------------------------------------------------------------------------- /testing/local-binder-k8s-hub/jupyterhub-chart-config-auth-additions.yaml: -------------------------------------------------------------------------------- 1 | # A JupyterHub Helm chart config containing only auth relevant config, and is 2 | # meant to be used alongside another configuration. 3 | 4 | hub: 5 | services: 6 | binder: 7 | oauth_no_confirm: true 8 | oauth_redirect_uri: "http://127.0.0.1:8585/oauth_callback" 9 | config: 10 | JupyterHub: 11 | authenticator_class: "dummy" 12 | DummyAuthenticator: 13 | password: "dummy" 14 | BinderSpawner: 15 | auth_enabled: true 16 | loadRoles: 17 | user: 18 | scopes: 19 | - self 20 | - "access:services!service=binder" 21 | -------------------------------------------------------------------------------- /testing/local-binder-k8s-hub/jupyterhub-chart-config.yaml: -------------------------------------------------------------------------------- 1 | # A JupyterHub Helm chart config for use whenever JupyterHub is deployed on 2 | # a kubernetes cluster. 3 | 4 | # NOTE: This is a mirror of the jupyterhub section in 5 | # binderhub-chart-config.yaml in testing/k8s-binder-k8s-hub, keep these 6 | # two files synced please. 7 | debug: 8 | enabled: true 9 | 10 | hub: 11 | config: 12 | BinderSpawner: 13 | cors_allow_origin: "*" 14 | db: 15 | type: "sqlite-memory" 16 | services: 17 | binder: 18 | # apiToken is also configured in 19 | # testing/local-binder-k8s-hub/binderhub_config.py 20 | apiToken: "dummy-binder-secret-token" 21 | 22 | proxy: 23 | service: 24 | type: NodePort 25 | nodePorts: 26 | http: 30902 27 | 28 | singleuser: 29 | storage: 30 | type: none 31 | memory: 32 | guarantee: null 33 | -------------------------------------------------------------------------------- /testing/local-binder-local-hub/README.md: -------------------------------------------------------------------------------- 1 | # A development config to test BinderHub locally without Kubernetes 2 | 3 | This runs `repo2docker` locally (_not_ in a container), then launches the built container in a local JupyterHub configured with DockerSpawner (or any container spawner that can use local container images). 4 | 5 | Install JupyterHub and dependencies 6 | 7 | pip install -r requirements.txt 8 | npm install -g configurable-http-proxy 9 | 10 | Install local BinderHub from source 11 | 12 | pip install -e ../.. 13 | 14 | Run JupyterHub in one terminal 15 | 16 | jupyterhub --config=jupyterhub_config.py 17 | 18 | BinderHub will be running as a managed JupyterHub service, go to http://localhost:8000 19 | and you should be redirected to BinderHub. 20 | 21 | If you want to test BinderHub with dummy authentication: 22 | 23 | export AUTHENTICATOR=dummy 24 | jupyterhub --config=jupyterhub_config.py 25 | -------------------------------------------------------------------------------- /testing/local-binder-local-hub/binderhub_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | A development config to test BinderHub locally. 3 | 4 | If you are running BinderHub manually (not via JupyterHub) run 5 | `python -m binderhub -f binderhub_config.py` 6 | 7 | Override the external access URL for JupyterHub by setting the 8 | environment variable JUPYTERHUB_EXTERNAL_URL 9 | Host IP is needed in a few places 10 | """ 11 | 12 | import os 13 | import socket 14 | 15 | from binderhub.build_local import LocalRepo2dockerBuild 16 | from binderhub.quota import LaunchQuota 17 | 18 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 19 | s.connect(("8.8.8.8", 80)) 20 | hostip = s.getsockname()[0] 21 | s.close() 22 | 23 | c.BinderHub.debug = True 24 | c.BinderHub.use_registry = False 25 | c.BinderHub.builder_required = False 26 | 27 | c.BinderHub.build_class = LocalRepo2dockerBuild 28 | c.BinderHub.push_secret = "" 29 | c.BinderHub.launch_quota_class = LaunchQuota 30 | 31 | c.BinderHub.about_message = "This is a local dev deployment without Kubernetes" 32 | c.BinderHub.banner_message = ( 33 | 'See BinderHub on GitHub' 34 | ) 35 | 36 | # Assert that we're running as a managed JupyterHub service 37 | # (otherwise c.BinderHub.hub_api_token is needed) 38 | assert os.getenv("JUPYTERHUB_API_TOKEN") 39 | c.BinderHub.base_url = os.getenv("JUPYTERHUB_SERVICE_PREFIX") 40 | 41 | c.BinderHub.hub_url = os.getenv("JUPYTERHUB_EXTERNAL_URL") or f"http://{hostip}:8000" 42 | 43 | if os.getenv("AUTH_ENABLED") == "1": 44 | c.BinderHub.auth_enabled = True 45 | -------------------------------------------------------------------------------- /testing/local-binder-local-hub/jupyterhub_config.py: -------------------------------------------------------------------------------- 1 | """ 2 | A development config to test BinderHub locally. 3 | 4 | Run `jupyterhub --config=binderhub_config.py` terminal 5 | Host IP is needed in a few places 6 | """ 7 | 8 | import os 9 | import socket 10 | 11 | from dockerspawner import DockerSpawner 12 | 13 | from binderhub.binderspawner_mixin import BinderSpawnerMixin 14 | 15 | 16 | def random_port() -> int: 17 | """Get a single random port.""" 18 | sock = socket.socket() 19 | sock.bind(("", 0)) 20 | port = sock.getsockname()[1] 21 | sock.close() 22 | return port 23 | 24 | 25 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 26 | s.connect(("8.8.8.8", 80)) 27 | hostip = s.getsockname()[0] 28 | s.close() 29 | 30 | 31 | # image & token are set via spawn options 32 | class LocalContainerSpawner(BinderSpawnerMixin, DockerSpawner): 33 | pass 34 | 35 | 36 | c.JupyterHub.spawner_class = LocalContainerSpawner 37 | c.DockerSpawner.remove = True 38 | c.DockerSpawner.allowed_images = "*" 39 | 40 | c.Application.log_level = "DEBUG" 41 | c.Spawner.debug = True 42 | c.JupyterHub.authenticator_class = os.getenv("AUTHENTICATOR", "null") 43 | 44 | auth_enabled = c.JupyterHub.authenticator_class != "null" 45 | if auth_enabled: 46 | c.LocalContainerSpawner.auth_enabled = True 47 | c.LocalContainerSpawner.cmd = "jupyterhub-singleuser" 48 | c.JupyterHub.load_roles = [ 49 | { 50 | "name": "user", 51 | "description": "Standard user privileges", 52 | "scopes": [ 53 | "self", 54 | "access:services!service=binder", 55 | ], 56 | } 57 | ] 58 | else: 59 | c.LocalContainerSpawner.cmd = "jupyter-notebook" 60 | 61 | c.JupyterHub.hub_ip = "0.0.0.0" 62 | c.JupyterHub.hub_connect_ip = hostip 63 | 64 | binderhub_service_name = "binder" 65 | binderhub_config = os.path.join(os.path.dirname(__file__), "binderhub_config.py") 66 | 67 | binderhub_environment = {} 68 | for env_var in ["JUPYTERHUB_EXTERNAL_URL", "GITHUB_ACCESS_TOKEN", "DOCKER_HOST"]: 69 | if os.getenv(env_var) is not None: 70 | binderhub_environment[env_var] = os.getenv(env_var) 71 | if auth_enabled: 72 | binderhub_environment["AUTH_ENABLED"] = "1" 73 | 74 | binderhub_port = random_port() 75 | 76 | c.JupyterHub.services = [ 77 | { 78 | "name": binderhub_service_name, 79 | "admin": True, 80 | "command": [ 81 | "python", 82 | "-mbinderhub", 83 | f"--config={binderhub_config}", 84 | f"--port={binderhub_port}", 85 | ], 86 | "url": f"http://localhost:{binderhub_port}", 87 | "environment": binderhub_environment, 88 | } 89 | ] 90 | c.JupyterHub.default_url = f"/services/{binderhub_service_name}/" 91 | 92 | c.JupyterHub.tornado_settings = { 93 | "slow_spawn_timeout": 0, 94 | } 95 | 96 | c.KubeSpawner.events_enabled = True 97 | -------------------------------------------------------------------------------- /testing/local-binder-local-hub/requirements.txt: -------------------------------------------------------------------------------- 1 | dockerspawner>=12 2 | jupyter-repo2docker>=2023.06.0 3 | jupyterhub>=3 4 | -------------------------------------------------------------------------------- /testing/local-binder-mocked-hub/binderhub_config.py: -------------------------------------------------------------------------------- 1 | # A development config to test BinderHub's UI. The image building or the 2 | # subsequent launching of the built image in a JupyterHub is mocked so that 3 | # users get stuck forever waiting for a build to complete. 4 | 5 | # Deployment assumptions: 6 | # - BinderHub: standalone local installation 7 | # - JupyterHub: mocked 8 | 9 | from binderhub.build import FakeBuild 10 | from binderhub.registry import FakeRegistry 11 | from binderhub.repoproviders import FakeProvider 12 | 13 | c.BinderHub.debug = True 14 | c.BinderHub.use_registry = True 15 | c.BinderHub.registry_class = FakeRegistry 16 | c.BinderHub.builder_required = False 17 | c.BinderHub.repo_providers = {"gh": FakeProvider} 18 | c.BinderHub.build_class = FakeBuild 19 | 20 | # Uncomment the following line to enable BinderHub's API only mode 21 | # With this, we can then use the `build_only` query parameter in the request 22 | # to not launch the image after build 23 | # c.BinderHub.enable_api_only_mode = True 24 | 25 | c.BinderHub.about_message = "Hello world." 26 | c.BinderHub.banner_message = 'This is headline news.' 27 | -------------------------------------------------------------------------------- /tools/generate-json-schema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | This script reads schema.yaml and generates a values.schema.json that we can 4 | package with the Helm chart, allowing helm the CLI to perform validation. 5 | 6 | While we can directly generate a values.schema.json from schema.yaml, it 7 | contains a lot of description text we use to generate our configuration 8 | reference that isn't helpful to ship along the validation schema. Due to that, 9 | we trim away everything that isn't needed. 10 | """ 11 | 12 | import json 13 | import os 14 | from collections.abc import MutableMapping 15 | 16 | from ruamel.yaml import YAML 17 | 18 | yaml = YAML() 19 | 20 | here_dir = os.path.abspath(os.path.dirname(__file__)) 21 | schema_yaml = os.path.join(here_dir, os.pardir, "helm-chart/binderhub", "schema.yaml") 22 | values_schema_json = os.path.join( 23 | here_dir, os.pardir, "helm-chart/binderhub", "values.schema.json" 24 | ) 25 | 26 | 27 | def clean_jsonschema(d, parent_key=""): 28 | """ 29 | Modifies a dictionary representing a jsonschema in place to not contain 30 | jsonschema keys not relevant for a values.schema.json file solely for use by 31 | the helm CLI. 32 | """ 33 | JSONSCHEMA_KEYS_TO_REMOVE = {"description"} 34 | 35 | # start by cleaning up the current level 36 | for k in set.intersection(JSONSCHEMA_KEYS_TO_REMOVE, set(d.keys())): 37 | del d[k] 38 | 39 | # Recursively cleanup nested levels, bypassing one level where there could 40 | # be a valid Helm chart configuration named just like the jsonschema 41 | # specific key to remove. 42 | if "properties" in d: 43 | for k, v in d["properties"].items(): 44 | if isinstance(v, MutableMapping): 45 | clean_jsonschema(v, k) 46 | 47 | 48 | def run(): 49 | # Using these sets, we can validate further manually by printing the results 50 | # of set operations. 51 | with open(schema_yaml) as f: 52 | schema = yaml.load(f) 53 | 54 | # Drop what isn't relevant for a values.schema.json file packaged with the 55 | # Helm chart, such as the description keys only relevant for our 56 | # configuration reference. 57 | clean_jsonschema(schema) 58 | 59 | # dump schema to values.schema.json 60 | with open(values_schema_json, "w") as f: 61 | json.dump(schema, f) 62 | 63 | print("binderhub/values.schema.json created") 64 | 65 | 66 | run() 67 | -------------------------------------------------------------------------------- /tools/validate-against-schema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | 4 | import jsonschema 5 | from ruamel.yaml import YAML 6 | 7 | yaml = YAML() 8 | 9 | here_dir = os.path.abspath(os.path.dirname(__file__)) 10 | schema_yaml = os.path.join(here_dir, os.pardir, "helm-chart/binderhub", "schema.yaml") 11 | values_yaml = os.path.join(here_dir, os.pardir, "helm-chart/binderhub", "values.yaml") 12 | lint_and_validate_values_yaml = os.path.join( 13 | here_dir, "templates", "lint-and-validate-values.yaml" 14 | ) 15 | binderhub_chart_config_yaml = os.path.join( 16 | here_dir, os.pardir, "testing/k8s-binder-k8s-hub", "binderhub-chart-config.yaml" 17 | ) 18 | 19 | with open(schema_yaml) as f: 20 | schema = yaml.load(f) 21 | with open(values_yaml) as f: 22 | values = yaml.load(f) 23 | with open(lint_and_validate_values_yaml) as f: 24 | lint_and_validate_values = yaml.load(f) 25 | with open(binderhub_chart_config_yaml) as f: 26 | binderhub_chart_config_yaml = yaml.load(f) 27 | 28 | # Validate values.yaml against schema 29 | print("Validating values.yaml against schema.yaml...") 30 | jsonschema.validate(values, schema) 31 | print("OK!") 32 | print() 33 | 34 | # Validate lint-and-validate-values.yaml against schema 35 | print("Validating lint-and-validate-values.yaml against schema.yaml...") 36 | jsonschema.validate(lint_and_validate_values, schema) 37 | print("OK!") 38 | print() 39 | 40 | # Validate lint-and-validate-values.yaml against schema 41 | print("Validating binderhub-chart-config.yaml against schema.yaml...") 42 | jsonschema.validate(lint_and_validate_values, schema) 43 | print("OK!") 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": false, 5 | "noEmit": false, 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true, 8 | "noImplicitAny": false, 9 | "module": "es6", 10 | "target": "es5", 11 | "jsx": "react-jsx", 12 | "moduleResolution": "node", 13 | "sourceMap": true 14 | }, 15 | "include": ["binderhub/static/js/"] 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const autoprefixer = require("autoprefixer"); 4 | 5 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 6 | 7 | module.exports = { 8 | mode: "development", 9 | context: path.resolve(__dirname, "binderhub/static"), 10 | entry: "./js/index.jsx", 11 | output: { 12 | path: path.resolve(__dirname, "binderhub/static/dist/"), 13 | filename: "bundle.js", 14 | publicPath: "auto", 15 | }, 16 | plugins: [ 17 | new MiniCssExtractPlugin({ 18 | filename: "styles.css", 19 | }), 20 | ], 21 | resolve: { 22 | extensions: [".tsx", ".ts", ".js", ".jsx"], 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.(t|j)sx?$/, 28 | exclude: /(node_modules|bower_components)/, 29 | use: { 30 | loader: "ts-loader", 31 | }, 32 | }, 33 | { 34 | test: /\.css$/i, 35 | use: [ 36 | { 37 | loader: MiniCssExtractPlugin.loader, 38 | options: { 39 | // Set publicPath as relative path ("./"). 40 | // By default it uses the `output.publicPath` ("/static/dist/"), when it rewrites the URLs in styles.css. 41 | // And it causes these files unavailabe if BinderHub has a different base_url than "/". 42 | publicPath: "./", 43 | }, 44 | }, 45 | "css-loader", 46 | ], 47 | }, 48 | { 49 | test: /\.(scss)$/, 50 | use: [ 51 | { 52 | // Adds CSS to the DOM by injecting a `